diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74a6eea..562fdeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,13 +29,13 @@ jobs: with: pixi-version: v0.81.0 run-install: false - global-environments: ffmpeg=7.1.1=gpl_* --with libclang=18 --with pkg-config + global-environments: ffmpeg=7.1.1=gpl_* --with libclang=18 --with pkg-config --with libheif --with libwebp --with nasm global-cache: true - name: Install build tools run: | python -m venv .venv .venv/bin/python -m pip install --upgrade pip - .venv/bin/pip install numpy pytest ruff 'maturin>=1.8,<2' + .venv/bin/pip install numpy pytest ruff 'maturin>=1.8,<2' 'Pillow>=11.3' cmake ninja meson echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV" - name: Configure prebuilt FFmpeg @@ -49,10 +49,15 @@ jobs: echo "PKG_CONFIG_PATH=$prefix/lib/pkgconfig" >> "$GITHUB_ENV" echo "BINDGEN_EXTRA_CLANG_ARGS=-I$(gcc -print-file-name=include)" >> "$GITHUB_ENV" "$prefix/bin/ffmpeg" -version + - name: Build dedicated image libraries + run: | + bash scripts/build_image_deps.sh "$PWD/.native-deps/images" + echo "PKG_CONFIG_PATH=$PWD/.native-deps/images/lib/pkgconfig:$PKG_CONFIG_PATH" >> "$GITHUB_ENV" + echo "LD_LIBRARY_PATH=$PWD/.native-deps/images/lib:$LD_LIBRARY_PATH" >> "$GITHUB_ENV" - name: Static checks run: | - ruff check src/tensorcodec tests scripts - ruff format --check src/tensorcodec tests scripts + ruff check src/tensorcodec tests scripts benchmarks/image_decode.py + ruff format --check src/tensorcodec tests scripts benchmarks/image_decode.py cargo fmt --manifest-path native/Cargo.toml --check cargo clippy --manifest-path native/Cargo.toml --locked -- -D warnings - name: Build extension against prebuilt FFmpeg @@ -60,6 +65,6 @@ jobs: - name: Install pinned reference run: python -m pip install torch==2.14.1 torchcodec==0.17.0 --index-url https://download.pytorch.org/whl/cpu - name: Validate independent contract against reference - run: pytest tests/test_video_contract.py tests/test_audio_contract.py --backend torchcodec + run: pytest tests/test_video_contract.py tests/test_audio_contract.py tests/test_images.py --backend torchcodec - name: Run contract and differential tests run: pytest --compare diff --git a/.github/workflows/macos-wheels.yml b/.github/workflows/macos-wheels.yml index 3976ae6..a1d4c45 100644 --- a/.github/workflows/macos-wheels.yml +++ b/.github/workflows/macos-wheels.yml @@ -31,9 +31,9 @@ jobs: global-cache: true - name: Install build tools run: | - brew install nasm pkg-config coreutils meson ninja + brew install nasm pkg-config coreutils meson ninja cmake uv venv - uv pip install numpy pytest 'maturin>=1.8,<2' delocate twine + uv pip install numpy pytest 'maturin>=1.8,<2' delocate twine 'Pillow>=11.3' echo "$PWD/.venv/bin" >> "$GITHUB_PATH" echo "LIBCLANG_PATH=$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib" >> "$GITHUB_ENV" echo "$HOME/.pixi/envs/ffmpeg/bin" >> "$GITHUB_PATH" @@ -41,7 +41,7 @@ jobs: id: native-cache with: path: .native-deps/macos - key: macos14-${{ matrix.arch }}-${{ hashFiles('scripts/build_openssl.sh', 'scripts/build_dav1d.sh', 'scripts/build_ffmpeg.sh', 'scripts/build_macos_wheel.sh') }} + key: macos14-${{ matrix.arch }}-${{ hashFiles('scripts/build_openssl.sh', 'scripts/build_dav1d.sh', 'scripts/build_ffmpeg.sh', 'scripts/build_image_deps.sh', 'scripts/build_macos_wheel.sh') }} - name: Build and bundle wheel run: bash scripts/build_macos_wheel.sh - uses: actions/cache/save@v6 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 00b2543..e679fcc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,7 +33,7 @@ jobs: id: native-cache with: path: .native-deps - key: manylinux2014-${{ matrix.target }}-${{ hashFiles('scripts/build_nasm.sh', 'scripts/build_openssl.sh', 'scripts/build_dav1d.sh', 'scripts/build_ffmpeg.sh') }} + key: manylinux2014-${{ matrix.target }}-${{ hashFiles('scripts/build_nasm.sh', 'scripts/build_openssl.sh', 'scripts/build_dav1d.sh', 'scripts/build_ffmpeg.sh', 'scripts/build_image_deps.sh') }} - name: Build portable Linux wheel uses: PyO3/maturin-action@v1 with: @@ -50,7 +50,7 @@ jobs: export LD_LIBRARY_PATH="$prefix/openssl/lib:$prefix/ffmpeg/lib" if [ ! -f "$prefix/ready" ]; then yum install -y perl-core - /opt/python/cp310-cp310/bin/python -m pip install meson==1.12.1 ninja==1.13.2 + /opt/python/cp310-cp310/bin/python -m pip install meson==1.12.1 ninja==1.13.2 cmake==4.4.3 export PATH="/opt/python/cp310-cp310/bin:$PATH" if [ "$(uname -m)" = x86_64 ]; then bash scripts/build_nasm.sh "$prefix/nasm" @@ -58,8 +58,11 @@ jobs: fi bash scripts/build_openssl.sh "$prefix/openssl" bash scripts/build_ffmpeg.sh "$prefix/ffmpeg" + bash scripts/build_image_deps.sh "$prefix/images" touch "$prefix/ready" fi + export PKG_CONFIG_PATH="$prefix/images/lib/pkgconfig:$PKG_CONFIG_PATH" + export LD_LIBRARY_PATH="$prefix/images/lib:$LD_LIBRARY_PATH" export FFMPEG_DIR="$prefix/ffmpeg" - name: Check native cache completeness if: always() @@ -87,7 +90,7 @@ jobs: sudo apt-get update sudo apt-get install -y ffmpeg uv venv - uv pip install pytest numpy twine dist/*.whl + uv pip install pytest numpy twine 'Pillow>=11.3' dist/*.whl - name: Generate baseline runtime fixtures run: uv run --no-sync python scripts/check_wheel_runtime.py generate .runtime-fixtures - name: Test on glibc 2.17 with Python 3.10 and 3.13 diff --git a/README.md b/README.md index 9de207a..59e55eb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # TensorCodec -CPU video/audio decoding with TorchCodec-style APIs and NumPy output. +CPU video/audio/image decoding with TorchCodec-style APIs and NumPy output.

CI @@ -18,7 +18,7 @@ CPU video/audio decoding with TorchCodec-style APIs and NumPy output. -- **TorchCodec API without PyTorch.** CPU video/audio decoder interfaces follow +- **TorchCodec API without PyTorch.** CPU video/audio/image decoder interfaces follow TorchCodec and return NumPy arrays. - **Validated playback semantics.** Frame selection, ordering, timestamps and audio ranges are checked against TorchCodec 0.17.0 and independently generated media. @@ -50,9 +50,25 @@ with AudioDecoder("audio.wav", sample_rate=16000, num_channels=1) as audio: waveform = samples.data # float32: (channels, samples) ``` -Arrays keep their storage after the decoder closes. Paths, URLs, encoded bytes, +Arrays keep their storage after the decoder closes. Video/audio paths, URLs, encoded bytes, 1-D uint8 arrays and seekable file objects are supported. +Image decoding uses TorchCodec's function API and returns CHW arrays (NCHW for animations): + +```python +from tensorcodec.decoders import decode_image, decode_jpeg, decode_png + +rgb = decode_image("image.webp") +batch = decode_jpeg(["left.jpg", "right.jpg"]) +depth = decode_png("depth.png", mode="UNCHANGED", output_dtype="auto") +``` + +Images use dedicated backends: Rust `png`, libjpeg-turbo, libwebp/libwebpdemux, +giflib and libavif. These are bundled in wheels; FFmpeg handles audio/video. +HEIC/HEIF optionally loads system `libheif`, as it does in TorchCodec. +No Pillow or PyTorch runtime dependency is added. See [image compatibility](docs/compatibility.md#images) +for color modes, high-depth conversion, input types and limitations. + For time-based windows without an initial full packet scan (since v0.1.4): ```python @@ -75,14 +91,14 @@ indices, `len(decoder)`, or `get_all_frames()`. See [the contract](docs/compatib ## Features -TensorCodec 0.1.5 relative to TorchCodec 0.17.0. +Current source relative to TorchCodec 0.17.0; image functions are not in the 0.1.5 release. ✓ supported · △ partial support · — not implemented. | Component | TensorCodec | TorchCodec 0.17.0 | | --- | --- | --- | | Video decoder | △ CPU, SDR/HDR RGB | ✓ CPU / CUDA | | Audio decoder | ✓ CPU | ✓ CPU | -| Image decoders | — | ✓ | +| Image decoders | △ CPU; HEIC needs optional system libheif | ✓ CPU; JPEG also CUDA | | Video / audio / image encoders | — | ✓ | | Clip samplers | — | ✓ | | Decoder transforms | ✓ Resize, CenterCrop, RandomCrop | ✓ | @@ -174,7 +190,10 @@ See [container behavior](docs/container_robustness.md) for seek limitations and

Build from source and run tests Source builds require Rust 1.88+, Clang/libclang, pkg-config and FFmpeg 7 development -headers/libraries. Python handles API and playback selection; Rust + PyO3 handles +headers/libraries, plus libjpeg-turbo, libwebp/libwebpdemux and libavif. +`scripts/build_image_deps.sh PREFIX` builds the image libraries; put +`PREFIX/lib/pkgconfig` in `PKG_CONFIG_PATH` and `PREFIX/lib` on the library search +path. It needs CMake, Ninja, Meson and NASM on x86. Python handles API and playback selection; Rust + PyO3 handles FFmpeg. Native decoding releases the GIL, allowing separate decoder instances to run concurrently across Python threads. Calls on the same instance are serialized. The default is one FFmpeg thread per decoder; use independent workers for concurrent diff --git a/benchmarks/image_candidates/Cargo.lock b/benchmarks/image_candidates/Cargo.lock new file mode 100644 index 0000000..ac41f09 --- /dev/null +++ b/benchmarks/image_candidates/Cargo.lock @@ -0,0 +1,496 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-candidate" +version = "0.0.0" +dependencies = [ + "image", + "libjpeg-turbo-rs", + "numpy", + "pyo3", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libjpeg-turbo-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d8a1c652b51dbb85c3c3164b1da63b88dafcc3fc12ecceb52f7577738c21f1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numpy" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94caae805f998a07d33af06e6a3891e38556051b8045c615470a71590e13e78" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "rustc-hash", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "pyo3" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/benchmarks/image_candidates/Cargo.toml b/benchmarks/image_candidates/Cargo.toml new file mode 100644 index 0000000..8eb01fa --- /dev/null +++ b/benchmarks/image_candidates/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "image-candidate" +version = "0.0.0" +edition = "2021" +[lib] +name = "_image_candidate" +crate-type = ["cdylib"] +[dependencies] +image = {version = "=0.25.10", default-features = false, features = ["jpeg", "png", "webp", "gif"]} +pyo3 = { version = "0.23", features = ["extension-module", "abi3-py310"] } +numpy = "0.23" +libjpeg-turbo-rs = "=0.8.0" +[profile.release] +lto = "thin" +codegen-units = 1 diff --git a/benchmarks/image_candidates/README.md b/benchmarks/image_candidates/README.md new file mode 100644 index 0000000..16c1a36 --- /dev/null +++ b/benchmarks/image_candidates/README.md @@ -0,0 +1,11 @@ +# Experimental image decoders + +A separate PyO3 module for comparing `image` and `libjpeg-turbo-rs` with the +production decoder. It only exercises still RGB uint8 decoding without EXIF +orientation; it is not an implementation of TensorCodec's full image contract. +Its crates do not enter the production dependency graph. + +Build with `maturin develop --release --locked --manifest-path +benchmarks/image_candidates/Cargo.toml --uv` inside the project's uv environment. +Run `benchmarks/image_decode.py --candidates --output results.json`. +See `docs/image-backends.md` for measurement scope and the recorded JSON reports. diff --git a/benchmarks/image_candidates/baseline.json b/benchmarks/image_candidates/baseline.json new file mode 100644 index 0000000..355bacc --- /dev/null +++ b/benchmarks/image_candidates/baseline.json @@ -0,0 +1,1838 @@ +[ + { + "case": "photo-224x224-JPEG420", + "format": "JPEG420", + "size": [ + 224, + 224 + ], + "content": "photo", + "bytes": 7319, + "ms": { + "native": 0.3127584233880043, + "image-rs": 0.36524999886751175, + "torchcodec": 0.32465483993291855, + "pillow": 0.41281867772340775 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.08925914115646258 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-224x224-JPEG444", + "format": "JPEG444", + "size": [ + 224, + 224 + ], + "content": "photo", + "bytes": 9503, + "ms": { + "native": 0.39221182465553284, + "image-rs": 0.43017029762268066, + "torchcodec": 0.4428016394376755, + "pillow": 0.5276620015501976 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.005141900510204082 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-224x224-JPEG-progressive", + "format": "JPEG-progressive", + "size": [ + 224, + 224 + ], + "content": "photo", + "bytes": 7082, + "ms": { + "native": 0.7591181993484497, + "image-rs": 0.7064919173717499, + "torchcodec": 0.738275982439518, + "pillow": 0.9229648354891185 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.08925914115646258 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-224x224-PNG", + "format": "PNG", + "size": [ + 224, + 224 + ], + "content": "photo", + "bytes": 33782, + "ms": { + "native": 1.2873747386038303, + "image-rs": 0.6173255667090416, + "torchcodec": 1.3290181884477879, + "pillow": 1.7386459830132397 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-224x224-WebP", + "format": "WebP", + "size": [ + 224, + 224 + ], + "content": "photo", + "bytes": 4998, + "ms": { + "native": 0.6611301282117533, + "image-rs": 1.1586261913180351, + "torchcodec": 0.6653646752238274, + "pillow": 0.9927565329953244 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-224x224-WebP-lossless", + "format": "WebP-lossless", + "size": [ + 224, + 224 + ], + "content": "photo", + "bytes": 25344, + "ms": { + "native": 0.9375779579083124, + "image-rs": 1.1547924174616735, + "torchcodec": 0.9382238392444218, + "pillow": 1.2726499699056149 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-JPEG420", + "format": "JPEG420", + "size": [ + 224, + 224 + ], + "content": "graphics", + "bytes": 22277, + "ms": { + "native": 0.38042251020669937, + "image-rs": 0.5173525214195251, + "torchcodec": 0.36453772336244583, + "pillow": 0.4894721880555153 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.23660714285714285 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-JPEG444", + "format": "JPEG444", + "size": [ + 224, + 224 + ], + "content": "graphics", + "bytes": 37271, + "ms": { + "native": 0.5577897280454636, + "image-rs": 0.7557127138842707, + "torchcodec": 0.5729645863175392, + "pillow": 0.6561538949608803 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.013565582482993197 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-JPEG-progressive", + "format": "JPEG-progressive", + "size": [ + 224, + 224 + ], + "content": "graphics", + "bytes": 18210, + "ms": { + "native": 0.8230883737697321, + "image-rs": 0.8117973804473877, + "torchcodec": 0.8778899791650474, + "pillow": 0.9067405692555688 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.23660714285714285 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-PNG", + "format": "PNG", + "size": [ + 224, + 224 + ], + "content": "graphics", + "bytes": 2238, + "ms": { + "native": 0.1794789731502533, + "image-rs": 0.06538886576890945, + "torchcodec": 0.19863706082105637, + "pillow": 0.6945323944091797 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-WebP", + "format": "WebP", + "size": [ + 224, + 224 + ], + "content": "graphics", + "bytes": 10720, + "ms": { + "native": 0.8062962046329011, + "image-rs": 1.7884713800057122, + "torchcodec": 0.8879966619941924, + "pillow": 1.2254928549130757 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-WebP-lossless", + "format": "WebP-lossless", + "size": [ + 224, + 224 + ], + "content": "graphics", + "bytes": 942, + "ms": { + "native": 0.09305238723754883, + "image-rs": 0.07142838090658188, + "torchcodec": 0.10076288133859634, + "pillow": 0.31766094267368317 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-JPEG420", + "format": "JPEG420", + "size": [ + 224, + 224 + ], + "content": "noise", + "bytes": 45654, + "ms": { + "native": 0.5997470877272018, + "image-rs": 0.8644462086078597, + "torchcodec": 0.6507079303264618, + "pillow": 0.6848244369029999 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.39372076955782315 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-JPEG444", + "format": "JPEG444", + "size": [ + 224, + 224 + ], + "content": "noise", + "bytes": 94423, + "ms": { + "native": 1.125846551183392, + "image-rs": 1.6684488139369271, + "torchcodec": 1.1703335950451512, + "pillow": 1.3484080055994647 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.047951211734693876 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-JPEG-progressive", + "format": "JPEG-progressive", + "size": [ + 224, + 224 + ], + "content": "noise", + "bytes": 41686, + "ms": { + "native": 1.9090279936790466, + "image-rs": 1.8878272351096659, + "torchcodec": 1.9059059106641345, + "pillow": 2.0485048880800605 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.39372076955782315 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-PNG", + "format": "PNG", + "size": [ + 224, + 224 + ], + "content": "noise", + "bytes": 150969, + "ms": { + "native": 0.8113250475038182, + "image-rs": 0.6878752633929253, + "torchcodec": 0.8377120608375186, + "pillow": 0.991958881849828 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-WebP", + "format": "WebP", + "size": [ + 224, + 224 + ], + "content": "noise", + "bytes": 42596, + "ms": { + "native": 3.0519476160407066, + "image-rs": 4.553463723924425, + "torchcodec": 3.0668975616043266, + "pillow": 3.3247578889131546 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-WebP-lossless", + "format": "WebP-lossless", + "size": [ + 224, + 224 + ], + "content": "noise", + "bytes": 150608, + "ms": { + "native": 0.9036101400852203, + "image-rs": 0.9088916920645292, + "torchcodec": 0.9548169560730457, + "pillow": 1.098965952063308 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-JPEG420", + "format": "JPEG420", + "size": [ + 640, + 480 + ], + "content": "photo", + "bytes": 26604, + "ms": { + "native": 1.1350397835485637, + "image-rs": 1.0665092955936084, + "torchcodec": 1.0454724165233407, + "pillow": 1.4978213994591325 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.05518229166666667 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-JPEG444", + "format": "JPEG444", + "size": [ + 640, + 480 + ], + "content": "photo", + "bytes": 35346, + "ms": { + "native": 1.415203486023278, + "image-rs": 1.4260286092758179, + "torchcodec": 1.445110576848189, + "pillow": 1.9144798187833083 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 2, + "mae": 0.0031890190972222224 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-JPEG-progressive", + "format": "JPEG-progressive", + "size": [ + 640, + 480 + ], + "content": "photo", + "bytes": 25304, + "ms": { + "native": 2.439423231408, + "image-rs": 2.3550416987675886, + "torchcodec": 2.4633731693029404, + "pillow": 2.9160166159272194 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.05518229166666667 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-PNG", + "format": "PNG", + "size": [ + 640, + 480 + ], + "content": "photo", + "bytes": 129509, + "ms": { + "native": 4.94668601701657, + "image-rs": 2.710741013288498, + "torchcodec": 5.0627666392496655, + "pillow": 7.046364868680636 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-WebP", + "format": "WebP", + "size": [ + 640, + 480 + ], + "content": "photo", + "bytes": 15206, + "ms": { + "native": 1.9486643373966217, + "image-rs": 4.177248726288478, + "torchcodec": 1.8505632877349854, + "pillow": 3.01711019128561 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-WebP-lossless", + "format": "WebP-lossless", + "size": [ + 640, + 480 + ], + "content": "photo", + "bytes": 91052, + "ms": { + "native": 2.989216779287045, + "image-rs": 4.555005114525557, + "torchcodec": 3.065078304364131, + "pillow": 4.572551231831312 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-JPEG420", + "format": "JPEG420", + "size": [ + 640, + 480 + ], + "content": "graphics", + "bytes": 131167, + "ms": { + "native": 2.157534472644329, + "image-rs": 2.867697738111019, + "torchcodec": 1.9658826487629038, + "pillow": 2.470696488252053 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.23579969618055555 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-JPEG444", + "format": "JPEG444", + "size": [ + 640, + 480 + ], + "content": "graphics", + "bytes": 224811, + "ms": { + "native": 3.23963656344197, + "image-rs": 4.558597380916278, + "torchcodec": 3.276688978075981, + "pillow": 3.4801305996047125 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.01448459201388889 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-JPEG-progressive", + "format": "JPEG-progressive", + "size": [ + 640, + 480 + ], + "content": "graphics", + "bytes": 106693, + "ms": { + "native": 4.547648131847382, + "image-rs": 4.274443619781071, + "torchcodec": 4.209839506074786, + "pillow": 4.727206891402602 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.23579969618055555 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-PNG", + "format": "PNG", + "size": [ + 640, + 480 + ], + "content": "graphics", + "bytes": 8698, + "ms": { + "native": 0.9408321369577337, + "image-rs": 0.36818157881498337, + "torchcodec": 0.8294916401306788, + "pillow": 3.8557611405849457 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-WebP", + "format": "WebP", + "size": [ + 640, + 480 + ], + "content": "graphics", + "bytes": 61612, + "ms": { + "native": 5.188380368053913, + "image-rs": 10.813632514327765, + "torchcodec": 5.443399505955832, + "pillow": 7.015836983919144 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-WebP-lossless", + "format": "WebP-lossless", + "size": [ + 640, + 480 + ], + "content": "graphics", + "bytes": 1190, + "ms": { + "native": 0.571601539850235, + "image-rs": 0.506751649081707, + "torchcodec": 0.4975387081503868, + "pillow": 1.848261182506879 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-JPEG420", + "format": "JPEG420", + "size": [ + 640, + 480 + ], + "content": "noise", + "bytes": 276032, + "ms": { + "native": 3.8927823305130005, + "image-rs": 5.621784366667271, + "torchcodec": 3.601166419684887, + "pillow": 4.300143362747298 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.3952756076388889 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-JPEG444", + "format": "JPEG444", + "size": [ + 640, + 480 + ], + "content": "noise", + "bytes": 575169, + "ms": { + "native": 6.912371516227722, + "image-rs": 10.272666346281767, + "torchcodec": 7.061867415904999, + "pillow": 7.84672312438488 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.04700629340277778 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-JPEG-progressive", + "format": "JPEG-progressive", + "size": [ + 640, + 480 + ], + "content": "noise", + "bytes": 251826, + "ms": { + "native": 11.19201754530271, + "image-rs": 11.100350568691889, + "torchcodec": 10.829218352834383, + "pillow": 12.105474869410196 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.3952756076388889 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-PNG", + "format": "PNG", + "size": [ + 640, + 480 + ], + "content": "noise", + "bytes": 923408, + "ms": { + "native": 5.721691197582653, + "image-rs": 5.869018534819285, + "torchcodec": 5.6426264345645905, + "pillow": 7.480774447321892 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-WebP", + "format": "WebP", + "size": [ + 640, + 480 + ], + "content": "noise", + "bytes": 257546, + "ms": { + "native": 19.369433633983135, + "image-rs": 27.72324625402689, + "torchcodec": 20.137750543653965, + "pillow": 20.832205191254616 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-WebP-lossless", + "format": "WebP-lossless", + "size": [ + 640, + 480 + ], + "content": "noise", + "bytes": 921680, + "ms": { + "native": 5.508032006522019, + "image-rs": 6.447361037135124, + "torchcodec": 5.438797441976411, + "pillow": 6.62788450717926 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-JPEG420", + "format": "JPEG420", + "size": [ + 1920, + 1080 + ], + "content": "photo", + "bytes": 98729, + "ms": { + "native": 7.071321830153465, + "image-rs": 5.631846686204274, + "torchcodec": 5.552360477546851, + "pillow": 8.911907207220793 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.02902665252057613 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-JPEG444", + "format": "JPEG444", + "size": [ + 1920, + 1080 + ], + "content": "photo", + "bytes": 136376, + "ms": { + "native": 7.584858685731888, + "image-rs": 6.8680766969919205, + "torchcodec": 7.775374129414558, + "pillow": 12.083444744348526 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.0017147312242798353 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-JPEG-progressive", + "format": "JPEG-progressive", + "size": [ + 1920, + 1080 + ], + "content": "photo", + "bytes": 87072, + "ms": { + "native": 12.977310456335545, + "image-rs": 12.064751237630844, + "torchcodec": 12.020805850625038, + "pillow": 16.94928202778101 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.02902665252057613 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-PNG", + "format": "PNG", + "size": [ + 1920, + 1080 + ], + "content": "photo", + "bytes": 482609, + "ms": { + "native": 32.42434095591307, + "image-rs": 16.17104560136795, + "torchcodec": 34.15762446820736, + "pillow": 44.46955490857363 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-WebP", + "format": "WebP", + "size": [ + 1920, + 1080 + ], + "content": "photo", + "bytes": 42772, + "ms": { + "native": 9.86394239589572, + "image-rs": 21.43648825585842, + "torchcodec": 9.004825260490179, + "pillow": 17.6306264474988 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-WebP-lossless", + "format": "WebP-lossless", + "size": [ + 1920, + 1080 + ], + "content": "photo", + "bytes": 307784, + "ms": { + "native": 16.135292127728462, + "image-rs": 27.97636017203331, + "torchcodec": 15.479901184638342, + "pillow": 24.237191304564476 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-JPEG420", + "format": "JPEG420", + "size": [ + 1920, + 1080 + ], + "content": "graphics", + "bytes": 886216, + "ms": { + "native": 12.604563807447752, + "image-rs": 19.774293527007103, + "torchcodec": 13.096215203404427, + "pillow": 17.12391246110201 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 11, + "mae": 0.23672453703703702 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-JPEG444", + "format": "JPEG444", + "size": [ + 1920, + 1080 + ], + "content": "graphics", + "bytes": 1514863, + "ms": { + "native": 22.611414082348347, + "image-rs": 31.751234084367752, + "torchcodec": 21.900122985243797, + "pillow": 26.588899083435535 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.014372749485596708 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-JPEG-progressive", + "format": "JPEG-progressive", + "size": [ + 1920, + 1080 + ], + "content": "graphics", + "bytes": 719741, + "ms": { + "native": 26.823491789400578, + "image-rs": 28.01576256752014, + "torchcodec": 26.786607690155506, + "pillow": 29.536647722125053 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 11, + "mae": 0.23672453703703702 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-PNG", + "format": "PNG", + "size": [ + 1920, + 1080 + ], + "content": "graphics", + "bytes": 44897, + "ms": { + "native": 5.706679075956345, + "image-rs": 2.349092314640681, + "torchcodec": 4.727693274617195, + "pillow": 24.81568604707718 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-WebP", + "format": "WebP", + "size": [ + 1920, + 1080 + ], + "content": "graphics", + "bytes": 412956, + "ms": { + "native": 34.77558586746454, + "image-rs": 67.97935720533133, + "torchcodec": 33.823488280177116, + "pillow": 42.73637942969799 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-WebP-lossless", + "format": "WebP-lossless", + "size": [ + 1920, + 1080 + ], + "content": "graphics", + "bytes": 3302, + "ms": { + "native": 3.400814634832469, + "image-rs": 3.052412635750241, + "torchcodec": 2.7663027867674828, + "pillow": 11.371756593386332 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-JPEG420", + "format": "JPEG420", + "size": [ + 1920, + 1080 + ], + "content": "noise", + "bytes": 1862057, + "ms": { + "native": 23.436269722878933, + "image-rs": 36.52063012123108, + "torchcodec": 23.252838291227818, + "pillow": 28.08335144072771 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 11, + "mae": 0.39605115097736626 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-JPEG444", + "format": "JPEG444", + "size": [ + 1920, + 1080 + ], + "content": "noise", + "bytes": 3876178, + "ms": { + "native": 47.563351690769196, + "image-rs": 70.44968940317631, + "torchcodec": 47.688371501863, + "pillow": 52.23469715565443 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.04752411265432099 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-JPEG-progressive", + "format": "JPEG-progressive", + "size": [ + 1920, + 1080 + ], + "content": "noise", + "bytes": 1697904, + "ms": { + "native": 75.28789155185223, + "image-rs": 76.82928442955017, + "torchcodec": 74.24560934305191, + "pillow": 78.53144127875566 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 11, + "mae": 0.39605115097736626 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-PNG", + "format": "PNG", + "size": [ + 1920, + 1080 + ], + "content": "noise", + "bytes": 6230665, + "ms": { + "native": 37.04366646707058, + "image-rs": 39.02777936309576, + "torchcodec": 36.3033777102828, + "pillow": 44.75824162364006 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-WebP", + "format": "WebP", + "size": [ + 1920, + 1080 + ], + "content": "noise", + "bytes": 1740812, + "ms": { + "native": 130.7628508657217, + "image-rs": 183.55221673846245, + "torchcodec": 129.4224513694644, + "pillow": 142.07238238304853 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-WebP-lossless", + "format": "WebP-lossless", + "size": [ + 1920, + 1080 + ], + "content": "noise", + "bytes": 6220880, + "ms": { + "native": 35.56007985025644, + "image-rs": 37.496001459658146, + "torchcodec": 33.8436895981431, + "pillow": 43.497588485479355 + }, + "diff_vs_torch": { + "native": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + } + } + } +] \ No newline at end of file diff --git a/benchmarks/image_candidates/selected.json b/benchmarks/image_candidates/selected.json new file mode 100644 index 0000000..b6da625 --- /dev/null +++ b/benchmarks/image_candidates/selected.json @@ -0,0 +1,1982 @@ +{ + "versions": { + "numpy": "2.5.3", + "Pillow": "12.3.0", + "torch": "2.14.1+cpu", + "torchcodec": "0.17.0+cpu" + }, + "method": "RGB uint8 CHW NumPy; in-memory bytes; one CPU affinity where available; 3 warmups; 5 rotated-order batches, median milliseconds; allocation/materialization included, encoding/IO excluded.", + "rows": [ + { + "case": "photo-224x224-JPEG420", + "format": "JPEG420", + "content": "photo", + "size": [ + 224, + 224 + ], + "bytes": 7319, + "ms": { + "tensorcodec": 0.31857434660196304, + "torchcodec": 0.34224290400743484, + "pillow": 0.42636651545763016, + "image-rs": 0.3203190863132477, + "turbo-rs": 0.29118459671735764 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.08925914115646258 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-224x224-JPEG444", + "format": "JPEG444", + "content": "photo", + "size": [ + 224, + 224 + ], + "bytes": 9503, + "ms": { + "tensorcodec": 0.43286535888910294, + "torchcodec": 0.4995769262313843, + "pillow": 0.6392958573997021, + "image-rs": 0.4139513522386551, + "turbo-rs": 0.38100067526102066 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.005141900510204082 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-224x224-JPEG-progressive", + "format": "JPEG-progressive", + "content": "photo", + "size": [ + 224, + 224 + ], + "bytes": 7082, + "ms": { + "tensorcodec": 0.7308598607778549, + "torchcodec": 0.8301981998717084, + "pillow": 0.9541254490613937, + "image-rs": 0.7720388368599944, + "turbo-rs": 0.604594387114048 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.08925914115646258 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-224x224-PNG", + "format": "PNG", + "content": "photo", + "size": [ + 224, + 224 + ], + "bytes": 33782, + "ms": { + "tensorcodec": 0.5952050702439414, + "torchcodec": 1.4787952861060267, + "pillow": 1.8200110644102097, + "image-rs": 0.572047894820571 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-224x224-WebP", + "format": "WebP", + "content": "photo", + "size": [ + 224, + 224 + ], + "bytes": 4998, + "ms": { + "tensorcodec": 0.6573878601193428, + "torchcodec": 0.7430371269583702, + "pillow": 1.049064442907509, + "image-rs": 1.1620032822801953 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-224x224-WebP-lossless", + "format": "WebP-lossless", + "content": "photo", + "size": [ + 224, + 224 + ], + "bytes": 25344, + "ms": { + "tensorcodec": 1.2196573118368785, + "torchcodec": 1.3500562248130639, + "pillow": 1.5884981575337322, + "image-rs": 1.482737793897589 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-JPEG420", + "format": "JPEG420", + "content": "graphics", + "size": [ + 224, + 224 + ], + "bytes": 22277, + "ms": { + "tensorcodec": 0.4848475754261017, + "torchcodec": 0.49743305891752243, + "pillow": 0.6501992046833038, + "image-rs": 0.6584399566054344, + "turbo-rs": 0.5766521021723747 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.23660714285714285 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-JPEG444", + "format": "JPEG444", + "content": "graphics", + "size": [ + 224, + 224 + ], + "bytes": 37271, + "ms": { + "tensorcodec": 0.7911321901997854, + "torchcodec": 0.770864401792371, + "pillow": 0.9289974937351738, + "image-rs": 1.0393480252888467, + "turbo-rs": 0.8355707344081666 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.013565582482993197 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-JPEG-progressive", + "format": "JPEG-progressive", + "content": "graphics", + "size": [ + 224, + 224 + ], + "bytes": 18210, + "ms": { + "tensorcodec": 0.8014668843575886, + "torchcodec": 0.850114066685949, + "pillow": 1.030466549338833, + "image-rs": 0.8630554151275883, + "turbo-rs": 0.7303407121645776 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.23660714285714285 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-PNG", + "format": "PNG", + "content": "graphics", + "size": [ + 224, + 224 + ], + "bytes": 2238, + "ms": { + "tensorcodec": 0.07903318852186203, + "torchcodec": 0.18142934888601303, + "pillow": 0.6957277655601501, + "image-rs": 0.07481440901756287 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-WebP", + "format": "WebP", + "content": "graphics", + "size": [ + 224, + 224 + ], + "bytes": 10720, + "ms": { + "tensorcodec": 0.875659328367975, + "torchcodec": 0.9136761470538813, + "pillow": 1.0982963123491831, + "image-rs": 1.8409066410227255 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-224x224-WebP-lossless", + "format": "WebP-lossless", + "content": "graphics", + "size": [ + 224, + 224 + ], + "bytes": 942, + "ms": { + "tensorcodec": 0.10981421917676926, + "torchcodec": 0.11028368026018143, + "pillow": 0.3275740519165993, + "image-rs": 0.0853242352604866 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-JPEG420", + "format": "JPEG420", + "content": "noise", + "size": [ + 224, + 224 + ], + "bytes": 45654, + "ms": { + "tensorcodec": 0.6600354984402657, + "torchcodec": 0.6627437099814415, + "pillow": 0.7012026757001877, + "image-rs": 0.9326254710172995, + "turbo-rs": 0.7948135158845356 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.39372076955782315 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-JPEG444", + "format": "JPEG444", + "content": "noise", + "size": [ + 224, + 224 + ], + "bytes": 94423, + "ms": { + "tensorcodec": 1.205207132126974, + "torchcodec": 1.2371704496186355, + "pillow": 1.2794286729051516, + "image-rs": 1.6961641171399284, + "turbo-rs": 1.4912406711474708 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.047951211734693876 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-JPEG-progressive", + "format": "JPEG-progressive", + "content": "noise", + "size": [ + 224, + 224 + ], + "bytes": 41686, + "ms": { + "tensorcodec": 1.931550563313067, + "torchcodec": 1.9034472294151783, + "pillow": 2.1171447281774722, + "image-rs": 1.9558018280400171, + "turbo-rs": 1.903941947966814 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.39372076955782315 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-PNG", + "format": "PNG", + "content": "noise", + "size": [ + 224, + 224 + ], + "bytes": 150969, + "ms": { + "tensorcodec": 0.6768934801220894, + "torchcodec": 0.8130884748332354, + "pillow": 1.161876107965197, + "image-rs": 0.7181339783639442 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-WebP", + "format": "WebP", + "content": "noise", + "size": [ + 224, + 224 + ], + "bytes": 42596, + "ms": { + "tensorcodec": 3.162715739260117, + "torchcodec": 3.3510403914584055, + "pillow": 3.471724858338183, + "image-rs": 4.397849552333355 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-224x224-WebP-lossless", + "format": "WebP-lossless", + "content": "noise", + "size": [ + 224, + 224 + ], + "bytes": 150608, + "ms": { + "tensorcodec": 0.9436152483287611, + "torchcodec": 0.9529475461352955, + "pillow": 1.239729593649055, + "image-rs": 1.0132033910070146 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-JPEG420", + "format": "JPEG420", + "content": "photo", + "size": [ + 640, + 480 + ], + "bytes": 26604, + "ms": { + "tensorcodec": 1.146871540130991, + "torchcodec": 1.116400325877799, + "pillow": 1.5953254132043748, + "image-rs": 1.1239162025352318, + "turbo-rs": 1.017719809267972 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.05518229166666667 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-JPEG444", + "format": "JPEG444", + "content": "photo", + "size": [ + 640, + 480 + ], + "bytes": 35346, + "ms": { + "tensorcodec": 1.604597507552667, + "torchcodec": 1.52358195434014, + "pillow": 1.9431747496128082, + "image-rs": 1.498384103178978, + "turbo-rs": 1.4502705695728462 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 2, + "mae": 0.0031890190972222224 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-JPEG-progressive", + "format": "JPEG-progressive", + "content": "photo", + "size": [ + 640, + 480 + ], + "bytes": 25304, + "ms": { + "tensorcodec": 2.7809899300336838, + "torchcodec": 2.603797987103462, + "pillow": 3.2667027643093696, + "image-rs": 2.60913609103723, + "turbo-rs": 2.1131577591101327 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.05518229166666667 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-PNG", + "format": "PNG", + "content": "photo", + "size": [ + 640, + 480 + ], + "bytes": 129509, + "ms": { + "tensorcodec": 2.9495301154943614, + "torchcodec": 5.674136492113273, + "pillow": 7.410284131765366, + "image-rs": 3.107984151159014 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-WebP", + "format": "WebP", + "content": "photo", + "size": [ + 640, + 480 + ], + "bytes": 15206, + "ms": { + "tensorcodec": 2.0915796566340656, + "torchcodec": 2.061296378572782, + "pillow": 3.1725110655481163, + "image-rs": 4.182923119515181 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-640x480-WebP-lossless", + "format": "WebP-lossless", + "content": "photo", + "size": [ + 640, + 480 + ], + "bytes": 91052, + "ms": { + "tensorcodec": 3.155436197465116, + "torchcodec": 3.138758671971468, + "pillow": 4.6454365365207195, + "image-rs": 5.034760261575381 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-JPEG420", + "format": "JPEG420", + "content": "graphics", + "size": [ + 640, + 480 + ], + "bytes": 131167, + "ms": { + "tensorcodec": 2.2139749179283776, + "torchcodec": 2.1379355134235487, + "pillow": 2.6796359568834305, + "image-rs": 3.0428431928157806, + "turbo-rs": 2.4614339837661157 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.23579969618055555 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-JPEG444", + "format": "JPEG444", + "content": "graphics", + "size": [ + 640, + 480 + ], + "bytes": 224811, + "ms": { + "tensorcodec": 3.4923026846213774, + "torchcodec": 3.4535364247858524, + "pillow": 3.8612400078111224, + "image-rs": 4.858455620706081, + "turbo-rs": 4.020118878947364 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.01448459201388889 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-JPEG-progressive", + "format": "JPEG-progressive", + "content": "graphics", + "size": [ + 640, + 480 + ], + "bytes": 106693, + "ms": { + "tensorcodec": 4.473647801205516, + "torchcodec": 4.491226514801383, + "pillow": 4.995031548397882, + "image-rs": 4.711949399539402, + "turbo-rs": 4.404824692755938 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.23579969618055555 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-PNG", + "format": "PNG", + "content": "graphics", + "size": [ + 640, + 480 + ], + "bytes": 8698, + "ms": { + "tensorcodec": 0.38608238101005554, + "torchcodec": 0.8783985461507525, + "pillow": 3.983151995473438, + "image-rs": 0.37378791719675064 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-WebP", + "format": "WebP", + "content": "graphics", + "size": [ + 640, + 480 + ], + "bytes": 61612, + "ms": { + "tensorcodec": 5.3199706599116325, + "torchcodec": 5.491009248154504, + "pillow": 6.52642734348774, + "image-rs": 10.53547610839208 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-640x480-WebP-lossless", + "format": "WebP-lossless", + "content": "graphics", + "size": [ + 640, + 480 + ], + "bytes": 1190, + "ms": { + "tensorcodec": 0.5465011671185493, + "torchcodec": 0.47845616936683655, + "pillow": 1.7706857373317082, + "image-rs": 0.48187538981437683 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-JPEG420", + "format": "JPEG420", + "content": "noise", + "size": [ + 640, + 480 + ], + "bytes": 276032, + "ms": { + "tensorcodec": 4.088966641575098, + "torchcodec": 3.770206951432758, + "pillow": 4.47437004186213, + "image-rs": 5.595280167957147, + "turbo-rs": 4.889658519199917 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.3952756076388889 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-JPEG444", + "format": "JPEG444", + "content": "noise", + "size": [ + 640, + 480 + ], + "bytes": 575169, + "ms": { + "tensorcodec": 7.208265736699104, + "torchcodec": 7.306059822440147, + "pillow": 7.7666472643613815, + "image-rs": 10.895750795801481, + "turbo-rs": 9.121447335928679 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.04700629340277778 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-JPEG-progressive", + "format": "JPEG-progressive", + "content": "noise", + "size": [ + 640, + 480 + ], + "bytes": 251826, + "ms": { + "tensorcodec": 11.966407919923464, + "torchcodec": 12.101354698340097, + "pillow": 12.778840959072113, + "image-rs": 12.027538691957792, + "turbo-rs": 12.019088491797447 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.3952756076388889 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-PNG", + "format": "PNG", + "content": "noise", + "size": [ + 640, + 480 + ], + "bytes": 923408, + "ms": { + "tensorcodec": 5.714292948444684, + "torchcodec": 5.6675756350159645, + "pillow": 7.116861330966155, + "image-rs": 5.893951281905174 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-WebP", + "format": "WebP", + "content": "noise", + "size": [ + 640, + 480 + ], + "bytes": 257546, + "ms": { + "tensorcodec": 21.053092554211617, + "torchcodec": 19.88494209945202, + "pillow": 21.356704644858837, + "image-rs": 28.863079845905304 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-640x480-WebP-lossless", + "format": "WebP-lossless", + "content": "noise", + "size": [ + 640, + 480 + ], + "bytes": 921680, + "ms": { + "tensorcodec": 5.708484777382442, + "torchcodec": 5.973499578734239, + "pillow": 7.054871693253517, + "image-rs": 6.385713070631027 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-JPEG420", + "format": "JPEG420", + "content": "photo", + "size": [ + 1920, + 1080 + ], + "bytes": 98729, + "ms": { + "tensorcodec": 6.335563425506864, + "torchcodec": 5.49193595846494, + "pillow": 9.587949762741724, + "image-rs": 5.700619891285896, + "turbo-rs": 5.504827148147991 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.02902665252057613 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-JPEG444", + "format": "JPEG444", + "content": "photo", + "size": [ + 1920, + 1080 + ], + "bytes": 136376, + "ms": { + "tensorcodec": 8.620034158229828, + "torchcodec": 7.849951460957527, + "pillow": 11.359576135873795, + "image-rs": 7.265032269060612, + "turbo-rs": 7.029488310217857 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.0017147312242798353 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-JPEG-progressive", + "format": "JPEG-progressive", + "content": "photo", + "size": [ + 1920, + 1080 + ], + "bytes": 87072, + "ms": { + "tensorcodec": 15.26867039501667, + "torchcodec": 14.494728917876879, + "pillow": 17.121857032179832, + "image-rs": 13.144444053371748, + "turbo-rs": 9.598095901310444 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 4, + "mae": 0.02902665252057613 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-PNG", + "format": "PNG", + "content": "photo", + "size": [ + 1920, + 1080 + ], + "bytes": 482609, + "ms": { + "tensorcodec": 15.714406967163086, + "torchcodec": 31.4980736002326, + "pillow": 44.9791606515646, + "image-rs": 16.21475163847208 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-WebP", + "format": "WebP", + "content": "photo", + "size": [ + 1920, + 1080 + ], + "bytes": 42772, + "ms": { + "tensorcodec": 9.72047820687294, + "torchcodec": 9.498042520135641, + "pillow": 17.567998729646206, + "image-rs": 23.38422928005457 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "photo-1920x1080-WebP-lossless", + "format": "WebP-lossless", + "content": "photo", + "size": [ + 1920, + 1080 + ], + "bytes": 307784, + "ms": { + "tensorcodec": 15.958595089614391, + "torchcodec": 14.285916462540627, + "pillow": 25.365088135004044, + "image-rs": 27.78587955981493 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-JPEG420", + "format": "JPEG420", + "content": "graphics", + "size": [ + 1920, + 1080 + ], + "bytes": 886216, + "ms": { + "tensorcodec": 14.202261343598366, + "torchcodec": 12.728787958621979, + "pillow": 16.65626000612974, + "image-rs": 20.312169566750526, + "turbo-rs": 16.15175511687994 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 11, + "mae": 0.23672453703703702 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-JPEG444", + "format": "JPEG444", + "content": "graphics", + "size": [ + 1920, + 1080 + ], + "bytes": 1514863, + "ms": { + "tensorcodec": 23.224574513733387, + "torchcodec": 21.20803203433752, + "pillow": 26.08264796435833, + "image-rs": 32.78392553329468, + "turbo-rs": 26.948368176817894 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.014372749485596708 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-JPEG-progressive", + "format": "JPEG-progressive", + "content": "graphics", + "size": [ + 1920, + 1080 + ], + "bytes": 719741, + "ms": { + "tensorcodec": 27.208897285163403, + "torchcodec": 27.110706083476543, + "pillow": 32.11980126798153, + "image-rs": 30.51014430820942, + "turbo-rs": 26.763678528368473 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 11, + "mae": 0.23672453703703702 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-PNG", + "format": "PNG", + "content": "graphics", + "size": [ + 1920, + 1080 + ], + "bytes": 44897, + "ms": { + "tensorcodec": 2.590323000081948, + "torchcodec": 5.2940680512360165, + "pillow": 26.613873429596424, + "image-rs": 2.6470717480954002 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-WebP", + "format": "WebP", + "content": "graphics", + "size": [ + 1920, + 1080 + ], + "bytes": 412956, + "ms": { + "tensorcodec": 34.27509963512421, + "torchcodec": 33.24252087622881, + "pillow": 42.87997633218765, + "image-rs": 65.82179758697748 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "graphics-1920x1080-WebP-lossless", + "format": "WebP-lossless", + "content": "graphics", + "size": [ + 1920, + 1080 + ], + "bytes": 3302, + "ms": { + "tensorcodec": 3.7732938812537626, + "torchcodec": 2.8427732487519584, + "pillow": 12.209602942069372, + "image-rs": 3.4918637635807195 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-JPEG420", + "format": "JPEG420", + "content": "noise", + "size": [ + 1920, + 1080 + ], + "bytes": 1862057, + "ms": { + "tensorcodec": 25.80876834690571, + "torchcodec": 24.29366484284401, + "pillow": 29.13115732371807, + "image-rs": 37.59732469916344, + "turbo-rs": 32.522029243409634 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 11, + "mae": 0.39605115097736626 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-JPEG444", + "format": "JPEG444", + "content": "noise", + "size": [ + 1920, + 1080 + ], + "bytes": 3876178, + "ms": { + "tensorcodec": 50.69590546190739, + "torchcodec": 46.8188039958477, + "pillow": 54.65860012918711, + "image-rs": 72.52769730985165, + "turbo-rs": 63.69514390826225 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 3, + "mae": 0.04752411265432099 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-JPEG-progressive", + "format": "JPEG-progressive", + "content": "noise", + "size": [ + 1920, + 1080 + ], + "bytes": 1697904, + "ms": { + "tensorcodec": 78.60574591904879, + "torchcodec": 78.19500006735325, + "pillow": 82.21063390374184, + "image-rs": 83.61110929399729, + "turbo-rs": 81.34329970926046 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 11, + "mae": 0.39605115097736626 + }, + "turbo-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-PNG", + "format": "PNG", + "content": "noise", + "size": [ + 1920, + 1080 + ], + "bytes": 6230665, + "ms": { + "tensorcodec": 43.06319821625948, + "torchcodec": 40.47695826739073, + "pillow": 51.192681305110455, + "image-rs": 43.271289207041264 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-WebP", + "format": "WebP", + "content": "noise", + "size": [ + 1920, + 1080 + ], + "bytes": 1740812, + "ms": { + "tensorcodec": 133.12074542045593, + "torchcodec": 128.27384192496538, + "pillow": 137.89507374167442, + "image-rs": 184.47029404342175 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + }, + { + "case": "noise-1920x1080-WebP-lossless", + "format": "WebP-lossless", + "content": "noise", + "size": [ + 1920, + 1080 + ], + "bytes": 6220880, + "ms": { + "tensorcodec": 36.694176495075226, + "torchcodec": 35.74570547789335, + "pillow": 61.238883063197136, + "image-rs": 40.24044517427683 + }, + "diff_vs_torch": { + "tensorcodec": { + "max": 0, + "mae": 0.0 + }, + "torchcodec": { + "max": 0, + "mae": 0.0 + }, + "pillow": { + "max": 0, + "mae": 0.0 + }, + "image-rs": { + "max": 0, + "mae": 0.0 + } + } + } + ] +} diff --git a/benchmarks/image_candidates/src/lib.rs b/benchmarks/image_candidates/src/lib.rs new file mode 100644 index 0000000..fda9a7d --- /dev/null +++ b/benchmarks/image_candidates/src/lib.rs @@ -0,0 +1,39 @@ +use numpy::{ndarray::Array, IntoPyArray}; +use pyo3::{exceptions::PyRuntimeError, prelude::*}; +#[pyfunction] +fn decode(py: Python<'_>, data: &[u8]) -> PyResult { + let (w, h, bytes) = py.allow_threads(|| { + let decoded = image::load_from_memory(data) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))? + .into_rgb8(); + Ok::<_, PyErr>(( + decoded.width() as usize, + decoded.height() as usize, + decoded.into_raw(), + )) + })?; + Ok(Array::from_shape_vec((h, w, 3), bytes) + .unwrap() + .into_pyarray(py) + .into_any() + .unbind()) +} +#[pyfunction] +fn decode_turbo(py: Python<'_>, data: &[u8]) -> PyResult { + let decoded = py + .allow_threads(|| libjpeg_turbo_rs::decompress(data)) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + Ok( + Array::from_shape_vec((decoded.height, decoded.width, 3), decoded.data) + .unwrap() + .into_pyarray(py) + .into_any() + .unbind(), + ) +} +#[pymodule] +fn _image_candidate(module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add_function(wrap_pyfunction!(decode, module)?)?; + module.add_function(wrap_pyfunction!(decode_turbo, module)?)?; + Ok(()) +} diff --git a/benchmarks/image_candidates/turbo-baseline.json b/benchmarks/image_candidates/turbo-baseline.json new file mode 100644 index 0000000..0f51dc5 --- /dev/null +++ b/benchmarks/image_candidates/turbo-baseline.json @@ -0,0 +1,272 @@ +[ + { + "case": "graphics-1920x1080-JPEG-progressive", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 34.69114191830158, + "native": 36.61426094671091, + "torchcodec": 35.90883376697699 + } + }, + { + "case": "graphics-1920x1080-JPEG420", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 21.381601691246033, + "native": 18.178485954801243, + "torchcodec": 13.013267889618874 + } + }, + { + "case": "graphics-1920x1080-JPEG444", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 27.711750318606693, + "native": 22.932900115847588, + "torchcodec": 21.885421747962635 + } + }, + { + "case": "graphics-224x224-JPEG-progressive", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 0.7627171774705251, + "native": 0.7504640767971674, + "torchcodec": 0.7183862229188284 + } + }, + { + "case": "graphics-224x224-JPEG420", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 0.347279633084933, + "native": 0.36513743301232654, + "torchcodec": 0.3487647821505864 + } + }, + { + "case": "graphics-224x224-JPEG444", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 0.7864739745855331, + "native": 0.5112874011198679, + "torchcodec": 0.5052462220191956 + } + }, + { + "case": "graphics-640x480-JPEG-progressive", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 3.9400619765122733, + "native": 4.5902834584315615, + "torchcodec": 4.6961574504772825 + } + }, + { + "case": "graphics-640x480-JPEG420", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 2.6120816667874656, + "native": 2.226502945025762, + "torchcodec": 2.045032878716787 + } + }, + { + "case": "graphics-640x480-JPEG444", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 4.021995390454928, + "native": 3.4574524809916816, + "torchcodec": 3.0931569635868073 + } + }, + { + "case": "noise-1920x1080-JPEG-progressive", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 80.59259566167991, + "native": 80.8087456971407, + "torchcodec": 81.58936103185017 + } + }, + { + "case": "noise-1920x1080-JPEG420", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 32.50303926567236, + "native": 25.776192545890808, + "torchcodec": 23.91505365570386 + } + }, + { + "case": "noise-1920x1080-JPEG444", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 65.77872360746066, + "native": 50.98629184067249, + "torchcodec": 47.58468642830849 + } + }, + { + "case": "noise-224x224-JPEG-progressive", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 2.025861293077469, + "native": 1.7654405285914738, + "torchcodec": 2.0584836602211 + } + }, + { + "case": "noise-224x224-JPEG420", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 0.7906022171179453, + "native": 0.5138721317052841, + "torchcodec": 0.5738207449515661 + } + }, + { + "case": "noise-224x224-JPEG444", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 1.5442731479803722, + "native": 0.9811607499917349, + "torchcodec": 1.0992561777432759 + } + }, + { + "case": "noise-640x480-JPEG-progressive", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 12.330721442898115, + "native": 11.764321476221085, + "torchcodec": 11.580652246872583 + } + }, + { + "case": "noise-640x480-JPEG420", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 4.512711117664973, + "native": 3.6549201856056848, + "torchcodec": 3.563984607656797 + } + }, + { + "case": "noise-640x480-JPEG444", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 9.538756062587103, + "native": 7.0970021188259125, + "torchcodec": 6.982412189245224 + } + }, + { + "case": "photo-1920x1080-JPEG-progressive", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 9.494865934054056, + "native": 13.631444424390793, + "torchcodec": 12.806867559750875 + } + }, + { + "case": "photo-1920x1080-JPEG420", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 5.3269583731889725, + "native": 6.593440969785054, + "torchcodec": 5.621532599131267 + } + }, + { + "case": "photo-1920x1080-JPEG444", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 6.716556226213773, + "native": 8.627496038873991, + "torchcodec": 7.963191096981366 + } + }, + { + "case": "photo-224x224-JPEG-progressive", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 0.4705619066953659, + "native": 0.44236953059832257, + "torchcodec": 0.5356657008330027 + } + }, + { + "case": "photo-224x224-JPEG420", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 0.19905405739943186, + "native": 0.17597712576389313, + "torchcodec": 0.23909409840901694 + } + }, + { + "case": "photo-224x224-JPEG444", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 0.26020656029383343, + "native": 0.23326960702737173, + "torchcodec": 0.30921151240666706 + } + }, + { + "case": "photo-640x480-JPEG-progressive", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 2.137914299964905, + "native": 3.06551530957222, + "torchcodec": 2.509774019320806 + } + }, + { + "case": "photo-640x480-JPEG420", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 1.0144089659055073, + "native": 1.1221083501974742, + "torchcodec": 1.0481613377730052 + } + }, + { + "case": "photo-640x480-JPEG444", + "max": 0, + "mae": 0.0, + "ms": { + "turbo-rs": 1.3342959185441334, + "native": 1.4365942527850468, + "torchcodec": 1.4675694207350414 + } + } +] \ No newline at end of file diff --git a/benchmarks/image_decode.py b/benchmarks/image_decode.py new file mode 100644 index 0000000..acb1eae --- /dev/null +++ b/benchmarks/image_decode.py @@ -0,0 +1,148 @@ +"""Compare complete bytes-to-RGB NumPy decoding; optional Rust backends are isolated.""" + +import argparse +import importlib.metadata +import io +import json +import os +import statistics +import subprocess +import time +from pathlib import Path + +import numpy as np +import torch +from PIL import Image +from torchcodec import decoders as oracle + +from tensorcodec import decoders + + +def inputs(root): + source = root / "tests/resources/nasa_13013.mp4" + encoded = subprocess.run( + [ + "ffmpeg", + "-v", + "error", + "-threads", + "1", + "-ss", + "1", + "-i", + str(source), + "-frames:v", + "1", + "-threads", + "1", + "-f", + "image2pipe", + "-c:v", + "png", + "-", + ], + capture_output=True, + check=True, + ).stdout + photo = Image.open(io.BytesIO(encoded)).convert("RGB") + for w, h in [(224, 224), (640, 480), (1920, 1080)]: + y, x = np.indices((h, w)) + patterns = { + "photo": photo.resize((w, h), Image.Resampling.LANCZOS), + "graphics": Image.fromarray( + np.stack(((x // 17 % 2) * 255, (y // 23 % 2) * 255, ((x + y) // 31 % 2) * 255), axis=-1).astype( + "uint8" + ) + ), + "noise": Image.fromarray(np.random.default_rng(42).integers(0, 256, (h, w, 3), dtype=np.uint8)), + } + formats = [ + ("JPEG420", "JPEG", {"quality": 90, "subsampling": 2}), + ("JPEG444", "JPEG", {"quality": 90, "subsampling": 0}), + ("JPEG-progressive", "JPEG", {"quality": 90, "progressive": True}), + ("PNG", "PNG", {}), + ("WebP", "WEBP", {"quality": 90}), + ("WebP-lossless", "WEBP", {"lossless": True}), + ] + for content, image in patterns.items(): + for name, fmt, options in formats: + buffer = io.BytesIO() + image.save(buffer, format=fmt, **options) + yield f"{content}-{w}x{h}-{name}", name, content, (w, h), buffer.getvalue() + + +def measure(calls): + repeats, times = {}, {name: [] for name in calls} + for name, fn in calls.items(): + for _ in range(3): + fn() + start = time.perf_counter() + fn() + elapsed = time.perf_counter() - start + repeats[name] = max(2, min(50, round(0.035 / max(elapsed, 1e-6)))) + for batch in range(5): + keys = list(calls) + keys = keys[batch % len(keys) :] + keys[: batch % len(keys)] + for name in keys: + start = time.perf_counter() + for _ in range(repeats[name]): + calls[name]() + times[name].append((time.perf_counter() - start) * 1000 / repeats[name]) + return {name: statistics.median(values) for name, values in times.items()} + + +def run(output, candidates): + if hasattr(os, "sched_getaffinity"): + os.sched_setaffinity(0, {min(os.sched_getaffinity(0))}) + torch.set_num_threads(1) + torch.set_num_interop_threads(1) + rust = None + if candidates: + import _image_candidate as rust + report = { + "versions": {name: importlib.metadata.version(name) for name in ("numpy", "Pillow", "torch", "torchcodec")}, + "method": "RGB uint8 CHW NumPy; in-memory bytes; one CPU affinity where available; 3 warmups; " + "5 rotated-order batches, median milliseconds; allocation/materialization included, encoding/IO excluded.", + "rows": [], + } + output.parent.mkdir(parents=True, exist_ok=True) + for case, fmt, content, size, data in inputs(Path(__file__).resolve().parents[1]): + + def pillow(data=data): + with Image.open(io.BytesIO(data)) as image: + return np.asarray(image.convert("RGB")).transpose(2, 0, 1) + + calls = { + "tensorcodec": lambda data=data: decoders.decode_image(data), + "torchcodec": lambda data=data: oracle.decode_image(data).numpy(), + "pillow": pillow, + } + if rust: + calls["image-rs"] = lambda data=data: rust.decode(data).transpose(2, 0, 1) + if fmt.startswith("JPEG"): + calls["turbo-rs"] = lambda data=data: rust.decode_turbo(data).transpose(2, 0, 1) + values = {name: fn() for name, fn in calls.items()} + diffs = {} + for name, value in values.items(): + delta = np.abs(value.astype(np.int16) - values["torchcodec"].astype(np.int16)) + diffs[name] = {"max": int(delta.max()), "mae": float(delta.mean())} + row = { + "case": case, + "format": fmt, + "content": content, + "size": size, + "bytes": len(data), + "ms": measure(calls), + "diff_vs_torch": diffs, + } + report["rows"].append(row) + output.write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(row), flush=True) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--candidates", action="store_true") + args = parser.parse_args() + run(args.output, args.candidates) diff --git a/docs/compatibility.md b/docs/compatibility.md index 41ada19..307777a 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,9 +1,63 @@ # TensorCodec compatibility contract -Reference: **TorchCodec 0.17.0**, CPU audio/video decoding. Python operations, +Reference: **TorchCodec 0.17.0**, CPU audio/video/image decoding. Python operations, frame selection, ordering, timestamps, durations, stream selection and metadata -are tested independently and against this pinned version. Native decoding uses -FFmpeg; arrays are returned as NumPy instead of torch.Tensor. +are tested independently and against this pinned version. Audio/video decoding uses FFmpeg; images use dedicated codec libraries. +Arrays are returned as NumPy instead of torch.Tensor. + +## Images + +`tensorcodec.decoders` exports `decode_image`, `decode_jpeg`, `decode_png`, +`decode_webp`, `decode_gif`, `decode_avif`, `decode_heic` and `ImageReadMode`. +They return arrays directly, not Frame objects. The defaults are `mode="RGB"` +and `output_dtype=np.uint8`. One image is CHW; animated/multi-image output is NCHW. + +- Inputs: local `str`/`Path` paths, encoded `bytes`/`bytearray`, or 1-D uint8 NumPy + arrays. Format detection uses encoded content, not the extension. File-like + inputs and URLs are not part of the image API. +- Modes: `UNCHANGED`, `GRAY`, `GRAY_ALPHA`, `RGB`, `RGB_ALPHA` (also `RGBA`). + Case-insensitive strings or `ImageReadMode` are accepted. Missing alpha becomes + fully opaque; existing alpha is retained when requested. +- JPEG/PNG/WebP EXIF orientation and AVIF rotation/mirroring are applied. + HEIC transformations are handled by libheif. +- Dtypes: NumPy uint8/uint16 or `"auto"`. PNG `UNCHANGED`/`auto` preserves native + 8/16-bit samples. Integer conversion scales the range, not merely the dtype: + uint8 to uint16 multiplies by 257. High-bit AVIF/HEIC yields full-range uint16, + not unscaled sensor codes. +- JPEG accepts a list/tuple and returns a list, preserving order and independent + storage. `device="cpu"` is the only supported device; CUDA fails explicitly. +- AVIF accepts `num_threads` (positive integer, default 1), passed to libavif. +- CMYK JPEG `UNCHANGED` returns CMYK samples; RGB/gray use the same conversion + as TorchCodec/Pillow. The fourth CMYK component is not alpha. + +Image decoding releases the GIL and does not use FFmpeg: + +| Format | Backend | Distribution | +| --- | --- | --- | +| JPEG | libjpeg-turbo 3.2.0 | Bundled shared library | +| PNG | Rust png 0.18.1 | Compiled into extension | +| WebP, including animation | libwebp/libwebpdemux 1.6.0 | Bundled shared libraries | +| GIF | giflib | Vendored from TorchCodec v0.17.0 | +| AVIF, including alpha and sequences | libavif 1.4.2, dav1d, libyuv | Bundled shared library | +| HEIC | System libheif | Optional; missing library raises ImportError | + +HEIC handles high depth, alpha and multiple images of equal shape, channel count +and bit depth, subject to the decoders compiled into libheif. Animated PNG is +unsupported and fails explicitly. No first-frame fallback is used for unsupported +animations. PNG UNCHANGED expands palette indices; non-palette tRNS keys retain +native channels, matching TorchCodec. Alpha modes expand those keys. + +PNG modes/dtypes, JPEG subsampling/progressive/CMYK, still/animated WebP, GIF +compositing, AVIF alpha/high depth/orientation and HEIC fixtures are tested against +TorchCodec. Textured JPEG/PNG/WebP fixtures compare exactly. This is sampled +compatibility, not a guarantee of bit-exact output for every codec version or +file. GIF RGB background composition intentionally follows TorchCodec; Pillow +uses different colors in some transparent regions. Color profiles are not +converted to sRGB and HDR is not tone-mapped. + +Image fixtures use known PNG sample values, independent FFmpeg/Pillow encodings, +and pinned upstream HEIC assets with their license. Pillow is test-only. +See [backend measurements](image-backends.md) for the selection evidence. ## Public surface diff --git a/docs/image-backends.md b/docs/image-backends.md new file mode 100644 index 0000000..6fa82c4 --- /dev/null +++ b/docs/image-backends.md @@ -0,0 +1,97 @@ +# Image backend selection + +The image API uses format-specific decoders rather than FFmpeg. Selection includes +pixel compatibility, decoded features, packaging and measured latency; neither C +nor Rust is assumed faster for every format. + +| Format | Selected backend | Evidence | +| --- | --- | --- | +| JPEG | libjpeg-turbo 3.2.0 | Exact sampled TorchCodec pixels; baseline JPEG faster than both Rust candidates | +| PNG | Rust png 0.18.1 | Exact native samples/modes after matching libpng rounding; faster than libpng on this corpus | +| WebP | libwebp/libwebpdemux 1.6.0 | Exact sampled pixels; faster than image-webp, including full animation compositing | +| GIF | giflib | TorchCodec-compatible palette/background/disposal rules | +| AVIF | libavif 1.4.2 + dav1d + libyuv | Same conversion path as TorchCodec; alpha, sequences, high depth and orientation | +| HEIC | Optional system libheif | Same library as TorchCodec; not bundled | + +The last three selections are primarily compatibility/feature decisions: this +benchmark does not establish their performance advantage over other libraries. + +## Reproducible comparison + +Measured on 2026-10-02, Linux x86-64, Intel Xeon Gold 6336Y, one CPU affinity. +Release builds use portable compiler defaults and runtime SIMD dispatch, not +`-march=native`. Python 3.12, NumPy 2.5.3, Pillow 12.3.0, TorchCodec 0.17.0 CPU. +The C-only prototype used libpng 1.6.59; the selected implementation uses Rust png. + +54 inputs: a frame from the repository's NASA video, deterministic colored +rectangles, and seeded noise, each at 224x224, 640x480 and 1920x1080. Each is +encoded as JPEG 4:2:0, JPEG 4:4:4, progressive JPEG, PNG, lossy WebP and lossless +WebP. These are three content patterns, not 54 independent photographs or a +representative production dataset. The previous FFmpeg experiment used only the +NASA frame; its results should not be compared as if the corpus were identical. + +The timed operation accepts identical encoded bytes in memory and returns a +fully decoded RGB uint8 CHW NumPy array. Pillow's lazy open alone is not timed: +RGB conversion and materialization are included. Torch's `.numpy()` is a view. +CHW does not require contiguous storage. Encoding, file I/O, imports and warmup +are excluded. Each case uses three warmups followed by five batches with rotated +backend order; the per-case result is the median milliseconds per image. Summary +ratios are geometric means of matched case latencies. Shared-machine scheduling +introduces noise; small differences are not conclusive. + +| Format (9 cases each) | Selected / TorchCodec latency | Selected / Pillow latency | Largest sampled difference vs TorchCodec | +| --- | ---: | ---: | ---: | +| JPEG 4:2:0 | 1.04x | 0.81x | 0 | +| JPEG 4:4:4 | 1.02x | 0.85x | 0 | +| Progressive JPEG | 0.99x | 0.87x | 0 | +| PNG | 0.59x | 0.30x | 0 | +| Lossy WebP | 0.99x | 0.78x | 0 | +| Lossless WebP | 1.05x | 0.54x | 0 | + +Lower latency ratios are better. This supports roughly TorchCodec-level JPEG/WebP +latency and about 1.7x faster PNG on these inputs, not universal speed claims. +No ARM performance claim follows from this x86 measurement. + +Candidates are image 0.25.10 (png 0.18.1, zune-jpeg 0.5.15, image-webp 0.2.4) +and libjpeg-turbo-rs 0.8.0, built as a separate PyO3/NumPy extension. In the final +run, image-rs JPEG was 22-23% slower for baseline JPEG and within 2% for progressive, +with maximum differences of 11/255 (subsampled) or 3/255 (4:4:4). Its WebP was +78% slower for lossy and 12% slower for lossless, with exact pixels. Its PNG +latency matched the selected Rust PNG implementation. libjpeg-turbo-rs was exact +on these 27 JPEGs, 7-8% slower for baseline JPEG and 11% faster for progressive. +It remains a credible alternative, but did not establish an overall advantage +for replacing the existing libjpeg-turbo choice. + +The initial C-vs-Rust run is `benchmarks/image_candidates/baseline.json`; the +separate JPEG-port check is `turbo-baseline.json`; the final rotated comparison +is `selected.json`. Differences between runs illustrate why small ratios should +not be treated as stable rankings. + +```sh +# Use an existing uv environment with the project dev/oracle groups installed. +uv run --no-sync maturin develop --release --locked --uv +uv run --no-sync maturin develop --release --locked \ + --manifest-path benchmarks/image_candidates/Cargo.toml --uv +OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 \ + uv run --no-sync python benchmarks/image_decode.py --candidates --output results.json +``` + +The experimental crates are not dependencies of TensorCodec and are not required +to use its image API. Tests separately cover native 16-bit PNG, all modes/dtypes, +Adam7, palette/transparency, orientations, malformed input, CMYK, animation, +AVIF alpha/high depth and licensed HEIC samples. Passing these tests is sampled +coverage, not certification of all images, versions or color profiles. + +## Reading external performance claims + +The [image-rs PNG report](https://blog.image-rs.org/2026/06/18/png-adoption.html) +uses 2,848 images from an independent corpus and compares against libpng with +zlib-ng. It also reports Chromium default adoption since M139, rather than only +an experiment. These are substantial signals for evaluating Rust PNG, but do not +rank JPEG/WebP or establish TensorCodec's end-to-end throughput. The report is +maintainer-authored and its hardware, workload and options still matter. + +The [libjpeg-turbo-rs report](https://github.com/developer0hye/libjpeg-turbo-rs) +is likewise a useful candidate source, not a substitute for matching pixel +semantics and measuring our wrapper. Encoder and resize benchmarks do not decide +this decoder-only API. Debug-build results are not deployment measurements. diff --git a/licenses/README.md b/licenses/README.md index 664e922..a32f77f 100644 --- a/licenses/README.md +++ b/licenses/README.md @@ -20,4 +20,15 @@ https://github.com/openssl/openssl/releases/download/openssl-3.5.9/openssl-3.5.9 System-library builds can additionally depend on Zstandard; its notices are retained here. Inspect repaired wheels when changing the native build. -PNG/Deflate decoding uses the platform zlib library. +Image decoding bundles libjpeg-turbo 3.2.0 (IJG / BSD-3-Clause / zlib), libwebp +1.6.0 (BSD-3-Clause), and libavif 1.4.2 (BSD-2-Clause). libavif includes dav1d +1.5.4 and libyuv commit 644251f252a84bf8ce91ff0aca86a9b16b069ab8 (BSD-3-Clause). +`scripts/build_image_deps.sh` pins their sources and build configuration. +PNG uses the Rust png 0.18.1 crate (MIT OR Apache-2.0), with Cargo.lock pinning its +transitive dependencies. GIF uses the MIT-licensed giflib sources vendored from +TorchCodec v0.17.0, whose provenance and notices are in native/vendor/giflib/README. +Image bindings follow TorchCodec's mode/conversion semantics; its BSD notice is +included in torchcodec.txt. + +HEIC optionally loads the user's system libheif; it is not bundled. The source distribution's +HEIC test assets retain TorchCodec's BSD license in `tests/resources/images/`. diff --git a/licenses/giflib.txt b/licenses/giflib.txt new file mode 100644 index 0000000..7353453 --- /dev/null +++ b/licenses/giflib.txt @@ -0,0 +1,28 @@ +These files come from the GIFLIB project (https://giflib.sourceforge.net/) and +are licensed under the MIT license. + +Some modifications have been made to the original files: +- Remove use of "register" keyword in gifalloc.c for C++17 compatibility. +- Declare loop variable i in DGifGetImageHeader as int instead of unsigned int. + +Below is the original license text from the COPYING file of the GIFLIB project: + += MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/licenses/libavif.txt b/licenses/libavif.txt new file mode 100644 index 0000000..350eb9d --- /dev/null +++ b/licenses/libavif.txt @@ -0,0 +1,387 @@ +Copyright 2019 Joe Drago. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: src/obu.c + +Copyright © 2018-2019, VideoLAN and dav1d authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: third_party/iccjpeg/* + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2013, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +------------------------------------------------------------------------------ + +Files: contrib/gdk-pixbuf/* + +Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------------------ + +Files: android_jni/gradlew* + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------------------ + +Files: third_party/libyuv/* + +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/libjpeg-ijg.txt b/licenses/libjpeg-ijg.txt new file mode 100644 index 0000000..dbf8070 --- /dev/null +++ b/licenses/libjpeg-ijg.txt @@ -0,0 +1,260 @@ +libjpeg-turbo note: This file has been modified by The libjpeg-turbo Project +to include only information relevant to libjpeg-turbo, to wordsmith certain +sections, and to remove impolitic language that existed in the libjpeg v8 +README. It is included only for reference. Please see README.md for +information specific to libjpeg-turbo. + + +The Independent JPEG Group's JPEG software +========================================== + +This distribution contains a release of the Independent JPEG Group's free JPEG +software. You are welcome to redistribute this software and to use it for any +purpose, subject to the conditions under LEGAL ISSUES, below. + +This software is the work of Tom Lane, Guido Vollbeding, Philip Gladstone, +Bill Allombert, Jim Boucher, Lee Crocker, Bob Friesenhahn, Ben Jackson, +Julian Minguillon, Luis Ortiz, George Phillips, Davide Rossi, Ge' Weijers, +and other members of the Independent JPEG Group. + +IJG is not affiliated with the ISO/IEC JTC1/SC29/WG1 standards committee +(also known as JPEG, together with ITU-T SG16). + + +DOCUMENTATION ROADMAP +===================== + +This file contains the following sections: + +OVERVIEW General description of JPEG and the IJG software. +LEGAL ISSUES Copyright, lack of warranty, terms of distribution. +REFERENCES Where to learn more about JPEG. +ARCHIVE LOCATIONS Where to find newer versions of this software. +FILE FORMAT WARS Software *not* to get. +TO DO Plans for future IJG releases. + +Other documentation files in the distribution are: + +User documentation: + doc/usage.txt Usage instructions for cjpeg, djpeg, jpegtran, + rdjpgcom, and wrjpgcom. + doc/*.1 Unix-style man pages for programs (same info as + usage.txt). + doc/wizard.txt Advanced usage instructions for JPEG wizards only. + doc/change.log Version-to-version change highlights. +Programmer and internal documentation: + doc/libjpeg.txt How to use the JPEG library in your own programs. + src/example.c Sample code for calling the JPEG library. + doc/structure.txt Overview of the JPEG library's internal structure. + doc/coderules.txt Coding style rules --- please read if you contribute + code. + +Please read at least usage.txt. Some information can also be found in the JPEG +FAQ (Frequently Asked Questions) article. See ARCHIVE LOCATIONS below to find +out where to obtain the FAQ article. + +If you want to understand how the JPEG code works, we suggest reading one or +more of the REFERENCES, then looking at the documentation files (in roughly +the order listed) before diving into the code. + + +OVERVIEW +======== + +This package contains C software to implement JPEG image encoding, decoding, +and transcoding. JPEG (pronounced "jay-peg") is a standardized compression +method for full-color and grayscale images. JPEG's strong suit is compressing +photographic images or other types of images that have smooth color and +brightness transitions between neighboring pixels. Images with sharp lines or +other abrupt features may not compress well with JPEG, and a higher JPEG +quality may have to be used to avoid visible compression artifacts with such +images. + +JPEG is normally lossy, meaning that the output pixels are not necessarily +identical to the input pixels. However, on photographic content and other +"smooth" images, very good compression ratios can be obtained with no visible +compression artifacts, and extremely high compression ratios are possible if +you are willing to sacrifice image quality (by reducing the "quality" setting +in the compressor.) + +This software implements JPEG baseline, extended-sequential, progressive, and +lossless compression processes. Provision is made for supporting all variants +of these processes, although some uncommon parameter settings aren't +implemented yet. We have made no provision for supporting the hierarchical +processes defined in the standard. + +We provide a set of library routines for reading and writing JPEG image files, +plus two sample applications "cjpeg" and "djpeg", which use the library to +perform conversion between JPEG and some other popular image file formats. +The library is intended to be reused in other applications. + +In order to support file conversion and viewing software, we have included +considerable functionality beyond the bare JPEG coding/decoding capability; +for example, the color quantization modules are not strictly part of JPEG +decoding, but they are essential for output to colormapped file formats. These +extra functions can be compiled out of the library if not required for a +particular application. + +We have also included "jpegtran", a utility for lossless transcoding between +different JPEG processes, and "rdjpgcom" and "wrjpgcom", two simple +applications for inserting and extracting textual comments in JFIF files. + +The emphasis in designing this software has been on achieving portability and +flexibility, while also making it fast enough to be useful. In particular, +the software is not intended to be read as a tutorial on JPEG. (See the +REFERENCES section for introductory material.) Rather, it is intended to +be reliable, portable, industrial-strength code. We do not claim to have +achieved that goal in every aspect of the software, but we strive for it. + +We welcome the use of this software as a component of commercial products. +No royalty is required, but we do ask for an acknowledgement in product +documentation, as described under LEGAL ISSUES. + + +LEGAL ISSUES +============ + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +REFERENCES +========== + +We recommend reading one or more of these references before trying to +understand the innards of the JPEG software. + +The best short technical introduction to the JPEG compression algorithm is + Wallace, Gregory K. "The JPEG Still Picture Compression Standard", + Communications of the ACM, April 1991 (vol. 34 no. 4), pp. 30-44. +(Adjacent articles in that issue discuss MPEG motion picture compression, +applications of JPEG, and related topics.) If you don't have the CACM issue +handy, a PDF file containing a revised version of Wallace's article is +available at http://www.ijg.org/files/Wallace.JPEG.pdf. The file (actually +a preprint for an article that appeared in IEEE Trans. Consumer Electronics) +omits the sample images that appeared in CACM, but it includes corrections +and some added material. Note: the Wallace article is copyright ACM and IEEE, +and it may not be used for commercial purposes. + +A somewhat less technical, more leisurely introduction to JPEG can be found in +"The Data Compression Book" by Mark Nelson and Jean-loup Gailly, published by +M&T Books (New York), 2nd ed. 1996, ISBN 1-55851-434-1. This book provides +good explanations and example C code for a multitude of compression methods +including JPEG. It is an excellent source if you are comfortable reading C +code but don't know much about data compression in general. The book's JPEG +sample code is far from industrial-strength, but when you are ready to look +at a full implementation, you've got one here... + +The best currently available description of JPEG is the textbook "JPEG Still +Image Data Compression Standard" by William B. Pennebaker and Joan L. +Mitchell, published by Van Nostrand Reinhold, 1993, ISBN 0-442-01272-1. +Price US$59.95, 638 pp. The book includes the complete text of the ISO JPEG +standards (DIS 10918-1 and draft DIS 10918-2). + +The original JPEG standard is divided into two parts, Part 1 being the actual +specification, while Part 2 covers compliance testing methods. Part 1 is +titled "Digital Compression and Coding of Continuous-tone Still Images, +Part 1: Requirements and guidelines" and has document numbers ISO/IEC IS +10918-1, ITU-T T.81. Part 2 is titled "Digital Compression and Coding of +Continuous-tone Still Images, Part 2: Compliance testing" and has document +numbers ISO/IEC IS 10918-2, ITU-T T.83. + +The JPEG standard does not specify all details of an interchangeable file +format. For the omitted details, we follow the "JFIF" conventions, revision +1.02. JFIF version 1 has been adopted as ISO/IEC 10918-5 (05/2013) and +Recommendation ITU-T T.871 (05/2011): Information technology - Digital +compression and coding of continuous-tone still images: JPEG File Interchange +Format (JFIF). It is available as a free download in PDF file format from +https://www.iso.org/standard/54989.html and http://www.itu.int/rec/T-REC-T.871. +A PDF file of the older JFIF 1.02 specification is available at +http://www.w3.org/Graphics/JPEG/jfif3.pdf. + +The TIFF 6.0 file format specification can be obtained from +http://mirrors.ctan.org/graphics/tiff/TIFF6.ps.gz. The JPEG incorporation +scheme found in the TIFF 6.0 spec of 3-June-92 has a number of serious +problems. IJG does not recommend use of the TIFF 6.0 design (TIFF Compression +tag 6). Instead, we recommend the JPEG design proposed by TIFF Technical Note +#2 (Compression tag 7). Copies of this Note can be obtained from +http://www.ijg.org/files/. It is expected that the next revision +of the TIFF spec will replace the 6.0 JPEG design with the Note's design. +Although IJG's own code does not support TIFF/JPEG, the free libtiff library +uses our library to implement TIFF/JPEG per the Note. + + +ARCHIVE LOCATIONS +================= + +The "official" archive site for this software is www.ijg.org. +The most recent released version can always be found there in +directory "files". + +The JPEG FAQ (Frequently Asked Questions) article is a source of some +general information about JPEG. It is available at +http://www.faqs.org/faqs/jpeg-faq. + + +FILE FORMAT COMPATIBILITY +========================= + +This software implements ITU T.81 | ISO/IEC 10918 with some extensions from +ITU T.871 | ISO/IEC 10918-5 (JPEG File Interchange Format-- see REFERENCES). +Informally, the term "JPEG image" or "JPEG file" most often refers to JFIF or +a subset thereof, but there are other formats containing the name "JPEG" that +are incompatible with the original JPEG standard or with JFIF (for instance, +JPEG 2000 and JPEG XR). This software therefore does not support these +formats. Indeed, one of the original reasons for developing this free software +was to help force convergence on a common, interoperable format standard for +JPEG files. + +JFIF is a minimal or "low end" representation. TIFF/JPEG (TIFF revision 6.0 as +modified by TIFF Technical Note #2) can be used for "high end" applications +that need to record a lot of additional data about an image. + + +TO DO +===== + +Please send bug reports, offers of help, etc. to jpeg-info@jpegclub.org. diff --git a/licenses/libjpeg-turbo.txt b/licenses/libjpeg-turbo.txt new file mode 100644 index 0000000..37a489b --- /dev/null +++ b/licenses/libjpeg-turbo.txt @@ -0,0 +1,147 @@ +libjpeg-turbo Licenses +====================== + +libjpeg-turbo is covered by two compatible BSD-style open source licenses: + +- The IJG (Independent JPEG Group) License, which is listed in + [README.ijg](README.ijg) + + This license applies to the libjpeg API library and associated programs, + including any code inherited from libjpeg and any modifications to that + code. + +- The Modified (3-clause) BSD License, which is listed below + + This license applies to the TurboJPEG API library and associated programs, + [libspng](https://libspng.org) (which is used by cjpeg and djpeg), and the + build/test system. + + * The TurboJPEG API library wraps the libjpeg API library, so in the context + of the overall TurboJPEG API library, both the terms of the IJG License and + the terms of the Modified (3-clause) BSD License apply. + * cjpeg and djpeg use libspng, so in the context of those programs, both the + terms of the IJG License and the terms of the Modified (3-clause) BSD + License apply. + + +Component Licenses +================== + +Some of libjpeg-turbo's modules and internal dependencies are covered by less +restrictive licenses, but in the context of libjpeg-turbo as a whole, the terms +of the less restrictive licenses are subsumed by either the IJG License or the +Modified BSD License. (In other words, the terms of the less restrictive +licenses are satisfied if the terms of the IJG and Modified BSD Licenses are +satisfied.) + +- The libjpeg-turbo SIMD source code and zlib are covered by the + [zlib License](https://spdx.org/licenses/Zlib.html), which is subsumed by the + IJG License in the context of the cjpeg and djpeg programs and the libjpeg + API library. + +- Some of the libspng source code is covered by the + [PNG Reference Library License v2](https://spdx.org/licenses/libpng-2.0.html), + which is subsumed by the IJG License in the context of the cjpeg and djpeg + programs and the TurboJPEG API library. + +- Most of the libspng source code is covered by the + [Simplified (2-clause) BSD License](https://spdx.org/licenses/BSD-2-Clause.html), + which is subsumed by the Modified BSD License in the context of the cjpeg and + djpeg programs and the TurboJPEG API library. + + +Complying with the libjpeg-turbo Licenses +========================================= + +This section provides a roll-up of the libjpeg-turbo licensing terms, to the +best of our understanding. This is not a license in and of itself. It is +intended solely for clarification. + +1. If you are distributing a modified version of the libjpeg-turbo source, + then: + + 1. You cannot alter or remove any existing copyright or license notices + from the source. + + **Origin** + - Clause 1 of the IJG License + - Clause 1 of the Modified BSD License + + 2. You must add your own copyright notice to the header of each source + file you modified, so others can tell that you modified that file. (If + there is not an existing copyright header in that file, then you can + simply add a notice stating that you modified the file.) + + **Origin** + - Clause 1 of the IJG License + + 3. You must include the IJG README file, and you must not alter any of the + copyright or license text in that file. + + **Origin** + - Clause 1 of the IJG License + +2. If you are distributing only libjpeg-turbo binaries without the source, or + if you are distributing an application that statically links with + libjpeg-turbo, then: + + 1. Your product documentation must include a message stating: + + This software is based in part on the work of the Independent JPEG + Group. + + **Origin** + - Clause 2 of the IJG license + + 2. If your binary distribution includes or uses the TurboJPEG API or + associated programs, cjpeg, or djpeg, then your product documentation + must include the text of the Modified BSD License (see below.) + + **Origin** + - Clause 2 of the Modified BSD License + +3. You cannot use the name of the IJG or The libjpeg-turbo Project or the + contributors thereof in advertising, publicity, etc. + + **Origin** + - IJG License + - Clause 3 of the Modified BSD License + +4. The authors and distributors do not warrant libjpeg-turbo to be free of + defects, nor do we accept any liability for undesirable consequences + resulting from your use of the software. + + **Origin** + - IJG License + - Modified BSD License + + +The Modified (3-clause) BSD License +=================================== + +Copyright (C) 2009-2026 D. R. Commander
+Copyright (C) 2018-2023 Randy + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +- Neither the name of the libjpeg-turbo Project nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/libwebp.txt b/licenses/libwebp.txt new file mode 100644 index 0000000..7a6f995 --- /dev/null +++ b/licenses/libwebp.txt @@ -0,0 +1,30 @@ +Copyright (c) 2010, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/licenses/libyuv.txt b/licenses/libyuv.txt new file mode 100644 index 0000000..c911747 --- /dev/null +++ b/licenses/libyuv.txt @@ -0,0 +1,29 @@ +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/png-rust.txt b/licenses/png-rust.txt new file mode 100644 index 0000000..bb1203c --- /dev/null +++ b/licenses/png-rust.txt @@ -0,0 +1,1692 @@ +adler2 2.0.1 — LICENSE-0BSD + +Copyright (C) Jonas Schievink + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +adler2 2.0.1 — LICENSE-APACHE + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/LICENSE-2.0 + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +adler2 2.0.1 — LICENSE-MIT + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +crc32fast 1.5.2 — LICENSE-APACHE + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +crc32fast 1.5.2 — LICENSE-MIT + +MIT License + +Copyright (c) 2018 Sam Rijs, Alex Crichton and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +fdeflate 0.3.7 — LICENSE-APACHE + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +fdeflate 0.3.7 — LICENSE-MIT + +MIT License + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +flate2 1.1.10 — LICENSE-APACHE + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +flate2 1.1.10 — LICENSE-MIT + +Copyright (c) 2014-2026 Alex Crichton + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +miniz_oxide 0.8.9 — LICENSE + +MIT License + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2017 Frommi +Copyright (c) 2017-2024 oyvindln + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +miniz_oxide 0.8.9 — LICENSE-APACHE.md + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +miniz_oxide 0.8.9 — LICENSE-MIT.md + +MIT License + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2017 Frommi +Copyright (c) 2017-2024 oyvindln + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +miniz_oxide 0.8.9 — LICENSE-ZLIB.md + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2020 Frommi +Copyright (c) 2017-2024 oyvindln + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +miniz_oxide 0.9.1 — LICENSE + +MIT License + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2017 Frommi +Copyright (c) 2017-2024 oyvindln + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +miniz_oxide 0.9.1 — LICENSE-APACHE.md + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +miniz_oxide 0.9.1 — LICENSE-MIT.md + +MIT License + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2017 Frommi +Copyright (c) 2017-2024 oyvindln + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +miniz_oxide 0.9.1 — LICENSE-ZLIB.md + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2020 Frommi +Copyright (c) 2017-2024 oyvindln + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + + +png 0.18.1 — LICENSE-APACHE + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +png 0.18.1 — LICENSE-MIT + +Copyright (c) 2015 nwin + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +simd-adler32 0.3.10 — LICENSE.md + +MIT License + +Copyright (c) [2021] [Marvin Countryman] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/torchcodec.txt b/licenses/torchcodec.txt new file mode 100644 index 0000000..bc559a9 --- /dev/null +++ b/licenses/torchcodec.txt @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright 2024 Meta + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice,this list +of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may +be used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/native/Cargo.lock b/native/Cargo.lock index 4e08ba5..f80b654 100644 --- a/native/Cargo.lock +++ b/native/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.5" @@ -77,12 +83,30 @@ dependencies = [ "libloading", ] +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + [[package]] name = "either" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ffmpeg-sys-next" version = "9.0.0" @@ -103,6 +127,17 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + [[package]] name = "glob" version = "0.3.4" @@ -186,6 +221,26 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "ndarray" version = "0.16.1" @@ -275,6 +330,19 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + [[package]] name = "portable-atomic" version = "1.15.0" @@ -430,6 +498,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "syn" version = "2.0.119" @@ -451,9 +525,12 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" name = "tensorcodec-native" version = "0.1.5" dependencies = [ + "cc", "ffmpeg-sys-next", "libc", "numpy", + "pkg-config", + "png", "pyo3", ] @@ -480,3 +557,9 @@ name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" diff --git a/native/Cargo.toml b/native/Cargo.toml index 8133547..11fb276 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -10,6 +10,7 @@ name = "_native" crate-type = ["cdylib"] [dependencies] +png = "=0.18.1" libc = "0.2" pyo3 = { version = "0.23", features = ["abi3-py310"] } numpy = "0.23" @@ -18,3 +19,7 @@ ffmpeg-sys-next = { version = "9.0", default-features = false, features = ["avco [profile.release] lto = "thin" codegen-units = 1 + +[build-dependencies] +cc = "1" +pkg-config = "0.3" diff --git a/native/build.rs b/native/build.rs new file mode 100644 index 0000000..4802312 --- /dev/null +++ b/native/build.rs @@ -0,0 +1,27 @@ +fn main() { + let mut build = cc::Build::new(); + for library in ["libjpeg", "libwebp", "libwebpdemux", "libavif"] { + let lib = pkg_config::Config::new() + .probe(library) + .unwrap_or_else(|e| { + panic!("{library}: {e}; run scripts/build_image_deps.sh and set PKG_CONFIG_PATH") + }); + for path in lib.include_paths { + build.include(path); + } + } + build.include("vendor/giflib").file("src/image_codecs.c"); + for source in [ + "dgif_lib.c", + "gifalloc.c", + "gif_hash.c", + "openbsd-reallocarray.c", + ] { + build.file(format!("vendor/giflib/{source}")); + } + build + .flag_if_supported("-std=gnu11") + .compile("image_codecs"); + println!("cargo:rerun-if-changed=src/image_codecs.c"); + println!("cargo:rerun-if-changed=vendor/giflib"); +} diff --git a/native/src/ffmpeg.rs b/native/src/ffmpeg.rs index a53a356..e4eea41 100644 --- a/native/src/ffmpeg.rs +++ b/native/src/ffmpeg.rs @@ -1330,6 +1330,7 @@ pub struct Video { pub height: usize, pub channels: usize, } + pub struct Audio { pub data: Vec, pub samples: usize, diff --git a/native/src/image_codecs.c b/native/src/image_codecs.c new file mode 100644 index 0000000..7cd78a6 --- /dev/null +++ b/native/src/image_codecs.c @@ -0,0 +1,283 @@ +/* Dedicated image codecs. All longjmp error handling stays within C. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gif_lib.h" + +typedef struct { + void *data; + size_t frames, height, width, channels, bytes; + int bits, orientation, animated; + char error[256]; +} Output; + +static int fail(Output *out, const char *message) { + if (message != out->error) snprintf(out->error, sizeof(out->error), "%s", message); + return 0; +} + +static int allocate(Output *out, size_t n, size_t h, size_t w, size_t c, int bits) { + size_t size = bits / 8; + const size_t dims[] = {n, h, w, c}; + for (size_t i = 0; i < 4; ++i) { + if (!dims[i] || dims[i] > (size_t)PTRDIFF_MAX / size) + return fail(out, "invalid or overflowing image dimensions"); + size *= dims[i]; + } + out->data = calloc(1, size); + if (!out->data) return fail(out, "cannot allocate image output"); + out->frames = n; out->height = h; out->width = w; out->channels = c; + out->bits = bits; out->bytes = size; + return 1; +} + +typedef struct { + struct jpeg_decompress_struct decoder; + struct jpeg_error_mgr error; + jmp_buf jump; + Output *out; + uint8_t *row; +} Jpeg; + +static void jpeg_error(j_common_ptr common) { + Jpeg *ctx = common->client_data; + char message[JMSG_LENGTH_MAX]; + common->err->format_message(common, message); + fail(ctx->out, message); + longjmp(ctx->jump, 1); +} +static void jpeg_message(j_common_ptr common, int level) { + if (level < 0) jpeg_error(common); /* Never synthesize missing EOI data. */ +} +static int decode_jpeg(const uint8_t *data, size_t size, int mode, Output *out) { + Jpeg *ctx = calloc(1, sizeof(*ctx)); + if (!ctx) return fail(out, "cannot allocate JPEG context"); + ctx->out = out; + ctx->decoder.err = jpeg_std_error(&ctx->error); + ctx->error.error_exit = jpeg_error; + ctx->error.emit_message = jpeg_message; + ctx->decoder.client_data = ctx; + if (setjmp(ctx->jump)) { + jpeg_destroy_decompress(&ctx->decoder); + free(ctx->row); free(ctx); + return 0; + } + jpeg_create_decompress(&ctx->decoder); + jpeg_mem_src(&ctx->decoder, data, size); + jpeg_read_header(&ctx->decoder, TRUE); + int cmyk = ctx->decoder.jpeg_color_space == JCS_CMYK || ctx->decoder.jpeg_color_space == JCS_YCCK; + int gray = mode == 1 || mode == 2; + if (!cmyk && mode) ctx->decoder.out_color_space = gray ? JCS_GRAYSCALE : JCS_RGB; + jpeg_start_decompress(&ctx->decoder); + size_t channels = cmyk && mode ? (gray ? 1 : 3) : (size_t)ctx->decoder.output_components; + if (!allocate(out, 1, ctx->decoder.output_height, ctx->decoder.output_width, channels, 8)) { + jpeg_destroy_decompress(&ctx->decoder); free(ctx); return 0; + } + if (cmyk && mode) { + ctx->row = malloc(out->width * 4); + if (!ctx->row) { + jpeg_destroy_decompress(&ctx->decoder); free(ctx); + return fail(out, "cannot allocate CMYK scanline"); + } + } + while (ctx->decoder.output_scanline < ctx->decoder.output_height) { + uint8_t *dest = (uint8_t *)out->data + ctx->decoder.output_scanline * out->width * channels; + uint8_t *row = ctx->row ? ctx->row : dest; + jpeg_read_scanlines(&ctx->decoder, &row, 1); + if (ctx->row) { + for (size_t x = 0; x < out->width; ++x) { + int rgb[3], k = row[4*x+3]; + for (int c = 0; c < 3; ++c) { + int v = k * (255 - row[4*x+c]) + 128; + rgb[c] = k - (((v >> 8) + v) >> 8); + } + if (gray) dest[x] = (rgb[0]*19595 + rgb[1]*38470 + rgb[2]*7471 + 32768) >> 16; + else for (int c = 0; c < 3; ++c) dest[3*x+c] = rgb[c]; + } + } else if (cmyk) { + for (size_t x = 0; x < out->width * 4; ++x) dest[x] = 255 - dest[x]; + } + } + jpeg_finish_decompress(&ctx->decoder); + jpeg_destroy_decompress(&ctx->decoder); + free(ctx->row); free(ctx); + return 1; +} + +typedef struct { const uint8_t *data; size_t left; Output *out; } Source; +static int decode_webp(const uint8_t *data, size_t size, int mode, Output *out) { + WebPBitstreamFeatures features; + if (WebPGetFeatures(data, size, &features) != VP8_STATUS_OK) return fail(out, "invalid WebP header"); + int channels = mode == 4 || mode == 2 || (!mode && features.has_alpha) ? 4 : 3; + if (!features.has_animation) { + if (!allocate(out, 1, features.height, features.width, channels, 8)) return 0; + if (out->width > INT_MAX / (size_t)channels) return fail(out, "WebP stride overflow"); + uint8_t *result = channels == 3 + ? WebPDecodeRGBInto(data, size, out->data, out->bytes, out->width * channels) + : WebPDecodeRGBAInto(data, size, out->data, out->bytes, out->width * channels); + return result ? 1 : fail(out, "invalid or truncated WebP"); + } + WebPData source = {data, size}; + WebPAnimDecoderOptions options; + if (!WebPAnimDecoderOptionsInit(&options)) return fail(out, "incompatible WebP animation ABI"); + options.color_mode = MODE_RGBA; + WebPAnimDecoder *decoder = WebPAnimDecoderNew(&source, &options); + if (!decoder) return fail(out, "invalid WebP animation"); + WebPAnimInfo info; + int ok = WebPAnimDecoderGetInfo(decoder, &info); + if (!ok || !allocate(out, info.frame_count, info.canvas_height, info.canvas_width, channels, 8)) { + WebPAnimDecoderDelete(decoder); return fail(out, "invalid WebP animation dimensions"); + } + out->animated = 1; + size_t i = 0, pixels = out->width * out->height; + while (WebPAnimDecoderHasMoreFrames(decoder)) { + uint8_t *frame; int timestamp; + if (i >= out->frames || !WebPAnimDecoderGetNext(decoder, &frame, ×tamp)) { ok = 0; break; } + uint8_t *dest = (uint8_t *)out->data + i++ * pixels * channels; + if (channels == 4) memcpy(dest, frame, pixels * 4); + else for (size_t j = 0; j < pixels; ++j) memcpy(dest + j*3, frame + j*4, 3); + } + WebPAnimDecoderDelete(decoder); + return ok && i == out->frames ? 1 : fail(out, "incomplete WebP animation"); +} + +static int decode_avif(const uint8_t *data, size_t size, int mode, int depth, int threads, Output *out) { + avifDecoder *decoder = avifDecoderCreate(); + if (!decoder) return fail(out, "cannot allocate AVIF decoder"); + decoder->maxThreads = threads; + avifResult result = avifDecoderSetIOMemory(decoder, data, size); + if (result == AVIF_RESULT_OK) result = avifDecoderParse(decoder); + if (result != AVIF_RESULT_OK) goto error; + avifImage *image = decoder->image; + int angle = image->transformFlags & AVIF_TRANSFORM_IROT ? image->irot.angle : 0; + int mirror = image->transformFlags & AVIF_TRANSFORM_IMIR; + const int rotations[] = {1, 8, 3, 6}, mirror0[] = {4, 5, 2, 7}, mirror1[] = {2, 7, 4, 5}; + out->orientation = mirror ? (image->imir.axis ? mirror1[angle & 3] : mirror0[angle & 3]) : rotations[angle & 3]; + size_t n = decoder->imageSequenceTrackPresent ? decoder->imageCount : 1; + int channels = mode == 4 || mode == 2 || (!mode && decoder->alphaPresent) ? 4 : 3; + int high = depth == 16 || (!depth && image->depth > 8); + if (!allocate(out, n, image->height, image->width, channels, high ? 16 : 8)) { avifDecoderDestroy(decoder); return 0; } + for (size_t i = 0; i < n; ++i) { + result = avifDecoderNextImage(decoder); + if (result != AVIF_RESULT_OK) goto error; + if (image->width != out->width || image->height != out->height) { + avifDecoderDestroy(decoder); return fail(out, "AVIF frames have different dimensions"); + } + avifRGBImage rgb; + avifRGBImageSetDefaults(&rgb, image); + rgb.depth = high ? 16 : 8; + rgb.format = channels == 3 ? AVIF_RGB_FORMAT_RGB : AVIF_RGB_FORMAT_RGBA; + rgb.ignoreAlpha = channels == 3; + rgb.pixels = (uint8_t *)out->data + i * out->bytes / n; + size_t stride = out->width * channels * (high ? 2 : 1); + if (stride > UINT32_MAX) { avifDecoderDestroy(decoder); return fail(out, "AVIF stride overflow"); } + rgb.rowBytes = (uint32_t)stride; + result = avifImageYUVToRGB(image, &rgb); + if (result != AVIF_RESULT_OK) goto error; + } + avifDecoderDestroy(decoder); return 1; +error: + fail(out, avifResultToString(result)); avifDecoderDestroy(decoder); return 0; +} + +static int gif_read(GifFileType *gif, GifByteType *dest, int size) { + Source *source = gif->UserData; + if (size < 0) return 0; + size_t n = (size_t)size < source->left ? (size_t)size : source->left; + memcpy(dest, source->data, n); source->data += n; source->left -= n; + return (int)n; +} +static void gif_clear(uint8_t *canvas, size_t h, size_t w, int c, GifColorType bg, GifImageDesc rect) { + for (int y = 0; y < rect.Height; ++y) { + size_t row = (size_t)rect.Top + y; + if (row >= h) continue; + for (int x = 0; x < rect.Width; ++x) { + size_t col = (size_t)rect.Left + x; + if (col >= w) continue; + uint8_t *p = canvas + (row*w + col)*c; + if (c == 4) memset(p, 0, 4); + else { p[0] = bg.Red; p[1] = bg.Green; p[2] = bg.Blue; } + } + } +} +static int decode_gif(const uint8_t *data, size_t size, int mode, Output *out) { + Source source = {data, size, out}; + int error = 0, ok = 0; + GifFileType *gif = DGifOpen(&source, gif_read, &error); + if (!gif) return fail(out, "invalid GIF header"); + uint8_t *restore = NULL; + if (DGifSlurp(gif) == GIF_ERROR || gif->ImageCount <= 0) { fail(out, "invalid or truncated GIF"); goto done; } + int alpha = 0; + for (int i = 0; i < gif->ImageCount; ++i) { + GraphicsControlBlock control = {0, false, 0, NO_TRANSPARENT_COLOR}; + DGifSavedExtensionToGCB(gif, i, &control); + alpha |= control.TransparentColor != NO_TRANSPARENT_COLOR; + } + int c = mode == 4 || mode == 2 || (!mode && alpha) ? 4 : 3; + size_t h = gif->SHeight, w = gif->SWidth; + if ((size_t)gif->SavedImages[0].ImageDesc.Height > h) h = gif->SavedImages[0].ImageDesc.Height; + if ((size_t)gif->SavedImages[0].ImageDesc.Width > w) w = gif->SavedImages[0].ImageDesc.Width; + if (!allocate(out, gif->ImageCount, h, w, c, 8)) goto done; + size_t frame_bytes = out->bytes / out->frames; + GifColorType bg = {0, 0, 0}; + if (gif->SColorMap) { + if (gif->SBackGroundColor < 0 || gif->SBackGroundColor >= gif->SColorMap->ColorCount) { fail(out, "invalid GIF background"); goto done; } + bg = gif->SColorMap->Colors[gif->SBackGroundColor]; + } + int disposal = 0; + GifImageDesc previous = {0}; + for (int i = 0; i < gif->ImageCount; ++i) { + uint8_t *canvas = (uint8_t *)out->data + i * frame_bytes; + GraphicsControlBlock control = {0, false, 0, NO_TRANSPARENT_COLOR}; + DGifSavedExtensionToGCB(gif, i, &control); + if (!i) { + GifImageDesc whole = {0}; whole.Width = w; whole.Height = h; + gif_clear(canvas, h, w, c, bg, whole); + } else if (disposal == DISPOSE_PREVIOUS && restore) memcpy(canvas, restore, frame_bytes); + else { + memcpy(canvas, canvas - frame_bytes, frame_bytes); + if (disposal == DISPOSE_BACKGROUND) gif_clear(canvas, h, w, c, bg, previous); + } + if (control.DisposalMode == DISPOSE_PREVIOUS) { + if (!restore) restore = malloc(frame_bytes); + if (!restore) { fail(out, "cannot allocate GIF restore canvas"); goto done; } + memcpy(restore, canvas, frame_bytes); + } + SavedImage *image = &gif->SavedImages[i]; + GifImageDesc desc = image->ImageDesc; + ColorMapObject *map = desc.ColorMap ? desc.ColorMap : gif->SColorMap; + if (!map) { fail(out, "GIF has no color map"); goto done; } + for (int y = 0; y < desc.Height; ++y) for (int x = 0; x < desc.Width; ++x) { + int index = image->RasterBits[(size_t)y*desc.Width+x]; + if (index == control.TransparentColor) continue; + if (index >= map->ColorCount) { fail(out, "invalid GIF palette index"); goto done; } + size_t row = (size_t)desc.Top+y, col = (size_t)desc.Left+x; + if (row >= h || col >= w) continue; + uint8_t *p = canvas + (row*w+col)*c; + p[0] = map->Colors[index].Red; p[1] = map->Colors[index].Green; p[2] = map->Colors[index].Blue; + if (c == 4) p[3] = 255; + } + disposal = control.DisposalMode; previous = desc; + } + ok = 1; +done: + free(restore); DGifCloseFile(gif, &error); return ok; +} + +int tc_decode_image(const uint8_t *data, size_t size, int codec, int mode, int depth, int threads, Output *out) { + out->orientation = 1; + switch (codec) { + case 0: return decode_jpeg(data, size, mode, out); + case 2: return decode_webp(data, size, mode, out); + case 3: return decode_gif(data, size, mode, out); + case 4: return decode_avif(data, size, mode, depth, threads, out); + default: return fail(out, "unknown image codec"); + } +} diff --git a/native/src/images.rs b/native/src/images.rs new file mode 100644 index 0000000..f62ac9b --- /dev/null +++ b/native/src/images.rs @@ -0,0 +1,102 @@ +//! Dedicated codec libraries; FFmpeg is reserved for audio/video. +use numpy::{ndarray::Array, IntoPyArray}; +use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::prelude::*; +use std::ffi::{c_char, c_int, c_void, CStr}; + +#[repr(C)] +struct Output { + data: *mut c_void, + frames: usize, + height: usize, + width: usize, + channels: usize, + bytes: usize, + bits: c_int, + orientation: c_int, + animated: c_int, + error: [c_char; 256], +} +impl Drop for Output { + fn drop(&mut self) { + unsafe { libc::free(self.data) }; + } +} +extern "C" { + fn tc_decode_image( + data: *const u8, + size: usize, + codec: c_int, + mode: c_int, + depth: c_int, + threads: c_int, + out: *mut Output, + ) -> c_int; +} + +#[pyfunction] +pub fn decode_image( + py: Python<'_>, + data: &[u8], + codec: i32, + mode: i32, + depth: i32, + threads: i32, +) -> PyResult<(PyObject, i32, bool)> { + if !(0..=4).contains(&codec) + || !(0..=4).contains(&mode) + || ![0, 8, 16].contains(&depth) + || threads < 1 + { + return Err(PyValueError::new_err("invalid native image options")); + } + if codec == 1 { + return Ok((crate::png_image::decode(py, data, mode, depth)?, 1, false)); + } + // Only the input slice crosses the GIL boundary. Output pointers stay on this thread. + let (pixels, shape, bits, orientation, animated) = py.allow_threads(|| { + let mut out: Output = unsafe { std::mem::zeroed() }; + let ok = unsafe { + tc_decode_image( + data.as_ptr(), + data.len(), + codec, + mode, + depth, + threads, + &mut out, + ) + }; + if ok == 0 { + return Err(PyRuntimeError::new_err( + unsafe { CStr::from_ptr(out.error.as_ptr()) } + .to_string_lossy() + .into_owned(), + )); + } + let shape = (out.frames, out.height, out.width, out.channels); + let bytes = + unsafe { std::slice::from_raw_parts(out.data.cast::(), out.bytes) }.to_vec(); + Ok((bytes, shape, out.bits, out.orientation, out.animated != 0)) + })?; + let array = if bits == 16 { + let values: Vec = pixels + .as_chunks::<2>() + .0 + .iter() + .map(|b| u16::from_ne_bytes(*b)) + .collect(); + Array::from_shape_vec(shape, values) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))? + .into_pyarray(py) + .into_any() + .unbind() + } else { + Array::from_shape_vec(shape, pixels) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))? + .into_pyarray(py) + .into_any() + .unbind() + }; + Ok((array, orientation, animated)) +} diff --git a/native/src/lib.rs b/native/src/lib.rs index 48a7951..26fc181 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,6 +1,8 @@ //! Only the FFmpeg boundary is native. Public API and playback planning live in Python. mod ffmpeg; +mod images; +mod png_image; use numpy::{ndarray::Array, IntoPyArray}; use pyo3::class::gc::{PyTraverseError, PyVisit}; @@ -136,6 +138,7 @@ fn closed() -> PyErr { #[pymodule] fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; + module.add_function(wrap_pyfunction!(images::decode_image, module)?)?; module.add("ffmpeg_version", ffmpeg::version())?; Ok(()) } diff --git a/native/src/png_image.rs b/native/src/png_image.rs new file mode 100644 index 0000000..a216f55 --- /dev/null +++ b/native/src/png_image.rs @@ -0,0 +1,146 @@ +//! PNG sample decoding in safe Rust, with TorchCodec/libpng mode semantics. +use numpy::{ndarray::Array, IntoPyArray}; +use png::{BitDepth, ColorType, Transformations}; +use pyo3::{exceptions::PyRuntimeError, prelude::*}; +use std::io::Cursor; + +struct Decoded { + pixels: Vec, + width: usize, + height: usize, + channels: usize, + bits: u8, +} +fn read(data: &[u8], mode: i32, depth: i32) -> Result { + let mut decoder = png::Decoder::new(Cursor::new(data)); + decoder.set_transformations(Transformations::EXPAND); + let mut reader = decoder.read_info().map_err(|e| e.to_string())?; + let source = reader.info(); + if source.animation_control.is_some() { + return Err("animated PNG decoding is unsupported".into()); + } + let native_channels = source.color_type.samples(); + let strip_transparency = + mode == 0 && source.color_type != ColorType::Indexed && source.trns.is_some(); + let size = reader + .output_buffer_size() + .ok_or("PNG dimensions overflow")?; + let mut pixels = Vec::new(); + pixels.try_reserve_exact(size).map_err(|e| e.to_string())?; + pixels.resize(size, 0); + let frame = reader.next_frame(&mut pixels).map_err(|e| e.to_string())?; + reader.finish().map_err(|e| e.to_string())?; + pixels.truncate(frame.buffer_size()); + let src_channels = frame.color_type.samples(); + let src_bits = if frame.bit_depth == BitDepth::Sixteen { + 16 + } else { + 8 + }; + let bits = if depth == 0 { src_bits } else { depth as u8 }; + let channels = if mode != 0 { + mode as usize + } else if strip_transparency { + native_channels + } else { + src_channels + }; + let (width, height) = (frame.width as usize, frame.height as usize); + if channels == src_channels && bits == 8 && src_bits == 8 { + return Ok(Decoded { + pixels, + width, + height, + channels, + bits, + }); + } + let length = width + .checked_mul(height) + .and_then(|n| n.checked_mul(channels)) + .and_then(|n| n.checked_mul((bits / 8) as usize)) + .ok_or("PNG output dimensions overflow")?; + let mut output = Vec::new(); + output + .try_reserve_exact(length) + .map_err(|e| e.to_string())?; + let maximum = if src_bits == 16 { 65535 } else { 255 }; + for pixel in pixels.chunks_exact(src_channels * (src_bits / 8) as usize) { + let mut values = [0u32; 4]; + for (i, value) in values.iter_mut().enumerate().take(src_channels) { + *value = if src_bits == 16 { + u16::from_be_bytes([pixel[2 * i], pixel[2 * i + 1]]) as u32 + } else { + pixel[i] as u32 + }; + } + let gray_source = src_channels <= 2; + let alpha = if src_channels == 2 || src_channels == 4 { + values[src_channels - 1] + } else { + maximum + }; + let gray = if gray_source { + values[0] + } else { + (values[0] * 9794 + + values[1] * 19234 + + values[2] * 3740 + + if src_bits == 16 { 16384 } else { 0 }) + >> 15 + }; + let converted = if channels <= 2 { + [gray, alpha, 0, 0] + } else if gray_source { + [values[0], values[0], values[0], alpha] + } else { + [values[0], values[1], values[2], alpha] + }; + for value in &converted[..channels] { + let value = match (src_bits, bits) { + (8, 16) => value * 257, + (16, 8) => (value + 128) / 257, + _ => *value, + }; + if bits == 16 { + output.extend_from_slice(&(value as u16).to_ne_bytes()); + } else { + output.push(value as u8); + } + } + } + Ok(Decoded { + pixels: output, + width, + height, + channels, + bits, + }) +} + +pub fn decode(py: Python<'_>, data: &[u8], mode: i32, depth: i32) -> PyResult { + let output = py + .allow_threads(|| read(data, mode, depth)) + .map_err(PyRuntimeError::new_err)?; + let shape = (1, output.height, output.width, output.channels); + if output.bits == 16 { + let values: Vec = output + .pixels + .as_chunks::<2>() + .0 + .iter() + .map(|b| u16::from_ne_bytes(*b)) + .collect(); + Ok(Array::from_shape_vec(shape, values) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))? + .into_pyarray(py) + .into_any() + .unbind()) + } else { + Ok(Array::from_shape_vec(shape, output.pixels) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))? + .into_pyarray(py) + .into_any() + .unbind()) + } +} diff --git a/native/vendor/giflib/README b/native/vendor/giflib/README new file mode 100644 index 0000000..985370e --- /dev/null +++ b/native/vendor/giflib/README @@ -0,0 +1,30 @@ +Vendored from pytorch/torchcodec v0.17.0, src/torchcodec/_core/giflib. + +These files come from the GIFLIB project (https://giflib.sourceforge.net/) and +are licensed under the MIT license. + +Some modifications have been made to the original files: +- Remove use of "register" keyword in gifalloc.c for C++17 compatibility. +- Declare loop variable i in DGifGetImageHeader as int instead of unsigned int. + +Below is the original license text from the COPYING file of the GIFLIB project: + += MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/native/vendor/giflib/dgif_lib.c b/native/vendor/giflib/dgif_lib.c new file mode 100644 index 0000000..7d35fff --- /dev/null +++ b/native/vendor/giflib/dgif_lib.c @@ -0,0 +1,1313 @@ +// @nolint (improperly imported third-party code) +/****************************************************************************** + +dgif_lib.c - GIF decoding + +The functions here and in egif_lib.c are partitioned carefully so that +if you only require one of read and write capability, only one of these +two modules will be linked. Preserve this property! + +*****************************************************************************/ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: Copyright (C) Eric S. Raymond + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif /* _WIN32 */ + +#include "gif_lib.h" +#include "gif_lib_private.h" + +/* compose unsigned little endian value */ +#define UNSIGNED_LITTLE_ENDIAN(lo, hi) ((lo) | ((hi) << 8)) + +/* avoid extra function call in case we use fread (TVT) */ +static int InternalRead(GifFileType *gif, GifByteType *buf, int len) { + // fprintf(stderr, "### Read: %d\n", len); + return (((GifFilePrivateType *)gif->Private)->Read + ? ((GifFilePrivateType *)gif->Private)->Read(gif, buf, len) + : fread(buf, 1, len, + ((GifFilePrivateType *)gif->Private)->File)); +} + +static int DGifGetWord(GifFileType *GifFile, GifWord *Word); +static int DGifSetupDecompress(GifFileType *GifFile); +static int DGifDecompressLine(GifFileType *GifFile, GifPixelType *Line, + int LineLen); +static int DGifGetPrefixChar(const GifPrefixType *Prefix, int Code, + int ClearCode); +static int DGifDecompressInput(GifFileType *GifFile, int *Code); +static int DGifBufferedInput(GifFileType *GifFile, GifByteType *Buf, + GifByteType *NextByte); + +/****************************************************************************** + Open a new GIF file for read, given by its name. + Returns dynamically allocated GifFileType pointer which serves as the GIF + info record. +******************************************************************************/ +GifFileType *DGifOpenFileName(const char *FileName, int *Error) { + int FileHandle; + GifFileType *GifFile; + + if ((FileHandle = open(FileName, O_RDONLY)) == -1) { + if (Error != NULL) { + *Error = D_GIF_ERR_OPEN_FAILED; + } + return NULL; + } + + GifFile = DGifOpenFileHandle(FileHandle, Error); + return GifFile; +} + +/****************************************************************************** + Update a new GIF file, given its file handle. + Returns dynamically allocated GifFileType pointer which serves as the GIF + info record. +******************************************************************************/ +GifFileType *DGifOpenFileHandle(int FileHandle, int *Error) { + char Buf[GIF_STAMP_LEN + 1]; + GifFileType *GifFile; + GifFilePrivateType *Private; + FILE *f; + + GifFile = (GifFileType *)malloc(sizeof(GifFileType)); + if (GifFile == NULL) { + if (Error != NULL) { + *Error = D_GIF_ERR_NOT_ENOUGH_MEM; + } + (void)close(FileHandle); + return NULL; + } + + /*@i1@*/ memset(GifFile, '\0', sizeof(GifFileType)); + + /* Belt and suspenders, in case the null pointer isn't zero */ + GifFile->SavedImages = NULL; + GifFile->SColorMap = NULL; + + Private = (GifFilePrivateType *)calloc(1, sizeof(GifFilePrivateType)); + if (Private == NULL) { + if (Error != NULL) { + *Error = D_GIF_ERR_NOT_ENOUGH_MEM; + } + (void)close(FileHandle); + free((char *)GifFile); + return NULL; + } + + /*@i1@*/ memset(Private, '\0', sizeof(GifFilePrivateType)); + +#ifdef _WIN32 + _setmode(FileHandle, O_BINARY); /* Make sure it is in binary mode. */ +#endif /* _WIN32 */ + + f = fdopen(FileHandle, "rb"); /* Make it into a stream: */ + + /*@-mustfreeonly@*/ + GifFile->Private = (void *)Private; + Private->FileHandle = FileHandle; + Private->File = f; + Private->FileState = FILE_STATE_READ; + Private->Read = NULL; /* don't use alternate input method (TVT) */ + GifFile->UserData = NULL; /* TVT */ + /*@=mustfreeonly@*/ + + /* Let's see if this is a GIF file: */ + /* coverity[check_return] */ + if (InternalRead(GifFile, (unsigned char *)Buf, GIF_STAMP_LEN) != + GIF_STAMP_LEN) { + if (Error != NULL) { + *Error = D_GIF_ERR_READ_FAILED; + } + (void)fclose(f); + free((char *)Private); + free((char *)GifFile); + return NULL; + } + + /* Check for GIF prefix at start of file */ + Buf[GIF_STAMP_LEN] = 0; + if (strncmp(GIF_STAMP, Buf, GIF_VERSION_POS) != 0) { + if (Error != NULL) { + *Error = D_GIF_ERR_NOT_GIF_FILE; + } + (void)fclose(f); + free((char *)Private); + free((char *)GifFile); + return NULL; + } + + if (DGifGetScreenDesc(GifFile) == GIF_ERROR) { + (void)fclose(f); + free((char *)Private); + free((char *)GifFile); + return NULL; + } + + GifFile->Error = 0; + + /* What version of GIF? */ + Private->gif89 = (Buf[GIF_VERSION_POS + 1] == '9'); + + return GifFile; +} + +/****************************************************************************** + GifFileType constructor with user supplied input function (TVT) +******************************************************************************/ +GifFileType *DGifOpen(void *userData, InputFunc readFunc, int *Error) { + char Buf[GIF_STAMP_LEN + 1]; + GifFileType *GifFile; + GifFilePrivateType *Private; + + GifFile = (GifFileType *)malloc(sizeof(GifFileType)); + if (GifFile == NULL) { + if (Error != NULL) { + *Error = D_GIF_ERR_NOT_ENOUGH_MEM; + } + return NULL; + } + + memset(GifFile, '\0', sizeof(GifFileType)); + + /* Belt and suspenders, in case the null pointer isn't zero */ + GifFile->SavedImages = NULL; + GifFile->SColorMap = NULL; + + Private = (GifFilePrivateType *)calloc(1, sizeof(GifFilePrivateType)); + if (!Private) { + if (Error != NULL) { + *Error = D_GIF_ERR_NOT_ENOUGH_MEM; + } + free((char *)GifFile); + return NULL; + } + /*@i1@*/ memset(Private, '\0', sizeof(GifFilePrivateType)); + + GifFile->Private = (void *)Private; + Private->FileHandle = 0; + Private->File = NULL; + Private->FileState = FILE_STATE_READ; + + Private->Read = readFunc; /* TVT */ + GifFile->UserData = userData; /* TVT */ + + /* Lets see if this is a GIF file: */ + /* coverity[check_return] */ + if (InternalRead(GifFile, (unsigned char *)Buf, GIF_STAMP_LEN) != + GIF_STAMP_LEN) { + if (Error != NULL) { + *Error = D_GIF_ERR_READ_FAILED; + } + free((char *)Private); + free((char *)GifFile); + return NULL; + } + + /* Check for GIF prefix at start of file */ + Buf[GIF_STAMP_LEN] = '\0'; + if (strncmp(GIF_STAMP, Buf, GIF_VERSION_POS) != 0) { + if (Error != NULL) { + *Error = D_GIF_ERR_NOT_GIF_FILE; + } + free((char *)Private); + free((char *)GifFile); + return NULL; + } + + if (DGifGetScreenDesc(GifFile) == GIF_ERROR) { + free((char *)Private); + free((char *)GifFile); + if (Error != NULL) { + *Error = D_GIF_ERR_NO_SCRN_DSCR; + } + return NULL; + } + + GifFile->Error = 0; + + /* What version of GIF? */ + Private->gif89 = (Buf[GIF_VERSION_POS + 1] == '9'); + + return GifFile; +} + +/****************************************************************************** + This routine should be called before any other DGif calls. Note that + this routine is called automatically from DGif file open routines. +******************************************************************************/ +int DGifGetScreenDesc(GifFileType *GifFile) { + int BitsPerPixel; + bool SortFlag; + GifByteType Buf[3]; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + /* Put the screen descriptor into the file: */ + if (DGifGetWord(GifFile, &GifFile->SWidth) == GIF_ERROR || + DGifGetWord(GifFile, &GifFile->SHeight) == GIF_ERROR) { + return GIF_ERROR; + } + + if (InternalRead(GifFile, Buf, 3) != 3) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + GifFreeMapObject(GifFile->SColorMap); + GifFile->SColorMap = NULL; + return GIF_ERROR; + } + GifFile->SColorResolution = (((Buf[0] & 0x70) + 1) >> 4) + 1; + SortFlag = (Buf[0] & 0x08) != 0; + BitsPerPixel = (Buf[0] & 0x07) + 1; + GifFile->SBackGroundColor = Buf[1]; + GifFile->AspectByte = Buf[2]; + if (Buf[0] & 0x80) { /* Do we have global color map? */ + int i; + + GifFile->SColorMap = GifMakeMapObject(1 << BitsPerPixel, NULL); + if (GifFile->SColorMap == NULL) { + GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + + /* Get the global color map: */ + GifFile->SColorMap->SortFlag = SortFlag; + for (i = 0; i < GifFile->SColorMap->ColorCount; i++) { + /* coverity[check_return] */ + if (InternalRead(GifFile, Buf, 3) != 3) { + GifFreeMapObject(GifFile->SColorMap); + GifFile->SColorMap = NULL; + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + GifFile->SColorMap->Colors[i].Red = Buf[0]; + GifFile->SColorMap->Colors[i].Green = Buf[1]; + GifFile->SColorMap->Colors[i].Blue = Buf[2]; + } + } else { + GifFile->SColorMap = NULL; + } + + /* + * No check here for whether the background color is in range for the + * screen color map. Possibly there should be. + */ + + return GIF_OK; +} + +const char *DGifGetGifVersion(GifFileType *GifFile) { + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (Private->gif89) { + return GIF89_STAMP; + } else { + return GIF87_STAMP; + } +} + +/****************************************************************************** + This routine should be called before any attempt to read an image. +******************************************************************************/ +int DGifGetRecordType(GifFileType *GifFile, GifRecordType *Type) { + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + /* coverity[check_return] */ + if (InternalRead(GifFile, &Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + + // fprintf(stderr, "### DGifGetRecordType: %02x\n", Buf); + switch (Buf) { + case DESCRIPTOR_INTRODUCER: + *Type = IMAGE_DESC_RECORD_TYPE; + break; + case EXTENSION_INTRODUCER: + *Type = EXTENSION_RECORD_TYPE; + break; + case TERMINATOR_INTRODUCER: + *Type = TERMINATE_RECORD_TYPE; + break; + default: + *Type = UNDEFINED_RECORD_TYPE; + GifFile->Error = D_GIF_ERR_WRONG_RECORD; + return GIF_ERROR; + } + + return GIF_OK; +} + +int DGifGetImageHeader(GifFileType *GifFile) { + unsigned int BitsPerPixel; + GifByteType Buf[3]; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + if (DGifGetWord(GifFile, &GifFile->Image.Left) == GIF_ERROR || + DGifGetWord(GifFile, &GifFile->Image.Top) == GIF_ERROR || + DGifGetWord(GifFile, &GifFile->Image.Width) == GIF_ERROR || + DGifGetWord(GifFile, &GifFile->Image.Height) == GIF_ERROR) { + return GIF_ERROR; + } + if (InternalRead(GifFile, Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + GifFreeMapObject(GifFile->Image.ColorMap); + GifFile->Image.ColorMap = NULL; + return GIF_ERROR; + } + BitsPerPixel = (Buf[0] & 0x07) + 1; + GifFile->Image.Interlace = (Buf[0] & 0x40) ? true : false; + + /* Setup the colormap */ + if (GifFile->Image.ColorMap) { + GifFreeMapObject(GifFile->Image.ColorMap); + GifFile->Image.ColorMap = NULL; + } + /* Does this image have local color map? */ + if (Buf[0] & 0x80) { + int i; + + GifFile->Image.ColorMap = + GifMakeMapObject(1 << BitsPerPixel, NULL); + if (GifFile->Image.ColorMap == NULL) { + GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + + /* Get the image local color map: */ + for (i = 0; i < GifFile->Image.ColorMap->ColorCount; i++) { + /* coverity[check_return] */ + if (InternalRead(GifFile, Buf, 3) != 3) { + GifFreeMapObject(GifFile->Image.ColorMap); + GifFile->Error = D_GIF_ERR_READ_FAILED; + GifFile->Image.ColorMap = NULL; + return GIF_ERROR; + } + GifFile->Image.ColorMap->Colors[i].Red = Buf[0]; + GifFile->Image.ColorMap->Colors[i].Green = Buf[1]; + GifFile->Image.ColorMap->Colors[i].Blue = Buf[2]; + } + } + + Private->PixelCount = + (long)GifFile->Image.Width * (long)GifFile->Image.Height; + + /* Reset decompress algorithm parameters. */ + return DGifSetupDecompress(GifFile); +} + +/****************************************************************************** + This routine should be called before any attempt to read an image. + Note it is assumed the Image desc. header has been read. +******************************************************************************/ +int DGifGetImageDesc(GifFileType *GifFile) { + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + SavedImage *sp; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + if (DGifGetImageHeader(GifFile) == GIF_ERROR) { + return GIF_ERROR; + } + + if (GifFile->SavedImages) { + SavedImage *new_saved_images = (SavedImage *)reallocarray( + GifFile->SavedImages, (GifFile->ImageCount + 1), + sizeof(SavedImage)); + if (new_saved_images == NULL) { + GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + GifFile->SavedImages = new_saved_images; + } else { + if ((GifFile->SavedImages = + (SavedImage *)malloc(sizeof(SavedImage))) == NULL) { + GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + } + + sp = &GifFile->SavedImages[GifFile->ImageCount]; + memcpy(&sp->ImageDesc, &GifFile->Image, sizeof(GifImageDesc)); + if (GifFile->Image.ColorMap != NULL) { + sp->ImageDesc.ColorMap = + GifMakeMapObject(GifFile->Image.ColorMap->ColorCount, + GifFile->Image.ColorMap->Colors); + if (sp->ImageDesc.ColorMap == NULL) { + GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM; + return GIF_ERROR; + } + } + sp->RasterBits = (unsigned char *)NULL; + sp->ExtensionBlockCount = 0; + sp->ExtensionBlocks = (ExtensionBlock *)NULL; + + GifFile->ImageCount++; + + return GIF_OK; +} + +/****************************************************************************** + Get one full scanned line (Line) of length LineLen from GIF file. +******************************************************************************/ +int DGifGetLine(GifFileType *GifFile, GifPixelType *Line, int LineLen) { + GifByteType *Dummy; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + if (!LineLen) { + LineLen = GifFile->Image.Width; + } + + if ((Private->PixelCount -= LineLen) > 0xffff0000UL) { + GifFile->Error = D_GIF_ERR_DATA_TOO_BIG; + return GIF_ERROR; + } + + if (DGifDecompressLine(GifFile, Line, LineLen) == GIF_OK) { + if (Private->PixelCount == 0) { + /* We probably won't be called any more, so let's clean + * up everything before we return: need to flush out all + * the rest of image until an empty block (size 0) + * detected. We use GetCodeNext. + */ + do { + if (DGifGetCodeNext(GifFile, &Dummy) == + GIF_ERROR) { + return GIF_ERROR; + } + } while (Dummy != NULL); + } + return GIF_OK; + } else { + return GIF_ERROR; + } +} + +/****************************************************************************** + Put one pixel (Pixel) into GIF file. +******************************************************************************/ +int DGifGetPixel(GifFileType *GifFile, GifPixelType Pixel) { + GifByteType *Dummy; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + if (--Private->PixelCount > 0xffff0000UL) { + GifFile->Error = D_GIF_ERR_DATA_TOO_BIG; + return GIF_ERROR; + } + + if (DGifDecompressLine(GifFile, &Pixel, 1) == GIF_OK) { + if (Private->PixelCount == 0) { + /* We probably won't be called any more, so let's clean + * up everything before we return: need to flush out all + * the rest of image until an empty block (size 0) + * detected. We use GetCodeNext. + */ + do { + if (DGifGetCodeNext(GifFile, &Dummy) == + GIF_ERROR) { + return GIF_ERROR; + } + } while (Dummy != NULL); + } + return GIF_OK; + } else { + return GIF_ERROR; + } +} + +/****************************************************************************** + Get an extension block (see GIF manual) from GIF file. This routine only + returns the first data block, and DGifGetExtensionNext should be called + after this one until NULL extension is returned. + The Extension should NOT be freed by the user (not dynamically allocated). + Note it is assumed the Extension description header has been read. +******************************************************************************/ +int DGifGetExtension(GifFileType *GifFile, int *ExtCode, + GifByteType **Extension) { + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + // fprintf(stderr, "### -> DGifGetExtension:\n"); + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + /* coverity[check_return] */ + if (InternalRead(GifFile, &Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + *ExtCode = Buf; + // fprintf(stderr, "### <- DGifGetExtension: %02x, about to call + // next\n", Buf); + + return DGifGetExtensionNext(GifFile, Extension); +} + +/****************************************************************************** + Get a following extension block (see GIF manual) from GIF file. This + routine should be called until NULL Extension is returned. + The Extension should NOT be freed by the user (not dynamically allocated). +******************************************************************************/ +int DGifGetExtensionNext(GifFileType *GifFile, GifByteType **Extension) { + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + // fprintf(stderr, "### -> DGifGetExtensionNext\n"); + if (InternalRead(GifFile, &Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + // fprintf(stderr, "### DGifGetExtensionNext sees %d\n", Buf); + + if (Buf > 0) { + *Extension = Private->Buf; /* Use private unused buffer. */ + (*Extension)[0] = + Buf; /* Pascal strings notation (pos. 0 is len.). */ + /* coverity[tainted_data,check_return] */ + if (InternalRead(GifFile, &((*Extension)[1]), Buf) != Buf) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + } else { + *Extension = NULL; + } + // fprintf(stderr, "### <- DGifGetExtensionNext: %p\n", Extension); + + return GIF_OK; +} + +/****************************************************************************** + Extract a Graphics Control Block from raw extension data +******************************************************************************/ + +int DGifExtensionToGCB(const size_t GifExtensionLength, + const GifByteType *GifExtension, + GraphicsControlBlock *GCB) { + if (GifExtensionLength != 4) { + return GIF_ERROR; + } + + GCB->DisposalMode = (GifExtension[0] >> 2) & 0x07; + GCB->UserInputFlag = (GifExtension[0] & 0x02) != 0; + GCB->DelayTime = + UNSIGNED_LITTLE_ENDIAN(GifExtension[1], GifExtension[2]); + if (GifExtension[0] & 0x01) { + GCB->TransparentColor = (int)GifExtension[3]; + } else { + GCB->TransparentColor = NO_TRANSPARENT_COLOR; + } + + return GIF_OK; +} + +/****************************************************************************** + Extract the Graphics Control Block for a saved image, if it exists. +******************************************************************************/ + +int DGifSavedExtensionToGCB(GifFileType *GifFile, int ImageIndex, + GraphicsControlBlock *GCB) { + int i; + + if (ImageIndex < 0 || ImageIndex > GifFile->ImageCount - 1) { + return GIF_ERROR; + } + + GCB->DisposalMode = DISPOSAL_UNSPECIFIED; + GCB->UserInputFlag = false; + GCB->DelayTime = 0; + GCB->TransparentColor = NO_TRANSPARENT_COLOR; + + for (i = 0; i < GifFile->SavedImages[ImageIndex].ExtensionBlockCount; + i++) { + ExtensionBlock *ep = + &GifFile->SavedImages[ImageIndex].ExtensionBlocks[i]; + if (ep->Function == GRAPHICS_EXT_FUNC_CODE) { + return DGifExtensionToGCB(ep->ByteCount, ep->Bytes, + GCB); + } + } + + return GIF_ERROR; +} + +/****************************************************************************** + This routine should be called last, to close the GIF file. +******************************************************************************/ +int DGifCloseFile(GifFileType *GifFile, int *ErrorCode) { + GifFilePrivateType *Private; + + if (GifFile == NULL || GifFile->Private == NULL) { + return GIF_ERROR; + } + + if (GifFile->Image.ColorMap) { + GifFreeMapObject(GifFile->Image.ColorMap); + GifFile->Image.ColorMap = NULL; + } + + if (GifFile->SColorMap) { + GifFreeMapObject(GifFile->SColorMap); + GifFile->SColorMap = NULL; + } + + if (GifFile->SavedImages) { + GifFreeSavedImages(GifFile); + GifFile->SavedImages = NULL; + } + + GifFreeExtensions(&GifFile->ExtensionBlockCount, + &GifFile->ExtensionBlocks); + + Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + if (ErrorCode != NULL) { + *ErrorCode = D_GIF_ERR_NOT_READABLE; + } + free((char *)GifFile->Private); + free(GifFile); + return GIF_ERROR; + } + + if (Private->File && (fclose(Private->File) != 0)) { + if (ErrorCode != NULL) { + *ErrorCode = D_GIF_ERR_CLOSE_FAILED; + } + free((char *)GifFile->Private); + free(GifFile); + return GIF_ERROR; + } + + free((char *)GifFile->Private); + free(GifFile); + if (ErrorCode != NULL) { + *ErrorCode = D_GIF_SUCCEEDED; + } + return GIF_OK; +} + +/****************************************************************************** + Get 2 bytes (word) from the given file: +******************************************************************************/ +static int DGifGetWord(GifFileType *GifFile, GifWord *Word) { + unsigned char c[2]; + + /* coverity[check_return] */ + if (InternalRead(GifFile, c, 2) != 2) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + + *Word = (GifWord)UNSIGNED_LITTLE_ENDIAN(c[0], c[1]); + return GIF_OK; +} + +/****************************************************************************** + Get the image code in compressed form. This routine can be called if the + information needed to be piped out as is. Obviously this is much faster + than decoding and encoding again. This routine should be followed by calls + to DGifGetCodeNext, until NULL block is returned. + The block should NOT be freed by the user (not dynamically allocated). +******************************************************************************/ +int DGifGetCode(GifFileType *GifFile, int *CodeSize, GifByteType **CodeBlock) { + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + *CodeSize = Private->BitsPerPixel; + + return DGifGetCodeNext(GifFile, CodeBlock); +} + +/****************************************************************************** + Continue to get the image code in compressed form. This routine should be + called until NULL block is returned. + The block should NOT be freed by the user (not dynamically allocated). +******************************************************************************/ +int DGifGetCodeNext(GifFileType *GifFile, GifByteType **CodeBlock) { + GifByteType Buf; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + /* coverity[tainted_data_argument] */ + /* coverity[check_return] */ + if (InternalRead(GifFile, &Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + + /* coverity[lower_bounds] */ + if (Buf > 0) { + *CodeBlock = Private->Buf; /* Use private unused buffer. */ + (*CodeBlock)[0] = + Buf; /* Pascal strings notation (pos. 0 is len.). */ + /* coverity[tainted_data] */ + if (InternalRead(GifFile, &((*CodeBlock)[1]), Buf) != Buf) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + } else { + *CodeBlock = NULL; + Private->Buf[0] = 0; /* Make sure the buffer is empty! */ + Private->PixelCount = + 0; /* And local info. indicate image read. */ + } + + return GIF_OK; +} + +/****************************************************************************** + Setup the LZ decompression for this image: +******************************************************************************/ +static int DGifSetupDecompress(GifFileType *GifFile) { + int i, BitsPerPixel; + GifByteType CodeSize; + GifPrefixType *Prefix; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + /* coverity[check_return] */ + if (InternalRead(GifFile, &CodeSize, 1) < + 1) { /* Read Code size from file. */ + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; /* Failed to read Code size. */ + } + BitsPerPixel = CodeSize; + + /* this can only happen on a severely malformed GIF */ + if (BitsPerPixel > 8) { + GifFile->Error = + D_GIF_ERR_READ_FAILED; /* somewhat bogus error code */ + return GIF_ERROR; /* Failed to read Code size. */ + } + + Private->Buf[0] = 0; /* Input Buffer empty. */ + Private->BitsPerPixel = BitsPerPixel; + Private->ClearCode = (1 << BitsPerPixel); + Private->EOFCode = Private->ClearCode + 1; + Private->RunningCode = Private->EOFCode + 1; + Private->RunningBits = BitsPerPixel + 1; /* Number of bits per code. */ + Private->MaxCode1 = 1 << Private->RunningBits; /* Max. code + 1. */ + Private->StackPtr = 0; /* No pixels on the pixel stack. */ + Private->LastCode = NO_SUCH_CODE; + Private->CrntShiftState = 0; /* No information in CrntShiftDWord. */ + Private->CrntShiftDWord = 0; + + Prefix = Private->Prefix; + for (i = 0; i <= LZ_MAX_CODE; i++) { + Prefix[i] = NO_SUCH_CODE; + } + + return GIF_OK; +} + +/****************************************************************************** + The LZ decompression routine: + This version decompress the given GIF file into Line of length LineLen. + This routine can be called few times (one per scan line, for example), in + order the complete the whole image. +******************************************************************************/ +static int DGifDecompressLine(GifFileType *GifFile, GifPixelType *Line, + int LineLen) { + int i = 0; + int j, CrntCode, EOFCode, ClearCode, CrntPrefix, LastCode, StackPtr; + GifByteType *Stack, *Suffix; + GifPrefixType *Prefix; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + StackPtr = Private->StackPtr; + Prefix = Private->Prefix; + Suffix = Private->Suffix; + Stack = Private->Stack; + EOFCode = Private->EOFCode; + ClearCode = Private->ClearCode; + LastCode = Private->LastCode; + + if (StackPtr > LZ_MAX_CODE) { + return GIF_ERROR; + } + + if (StackPtr != 0) { + /* Let pop the stack off before continueing to read the GIF + * file: */ + while (StackPtr != 0 && i < LineLen) { + Line[i++] = Stack[--StackPtr]; + } + } + + while (i < LineLen) { /* Decode LineLen items. */ + if (DGifDecompressInput(GifFile, &CrntCode) == GIF_ERROR) { + return GIF_ERROR; + } + + if (CrntCode == EOFCode) { + /* Note however that usually we will not be here as we + * will stop decoding as soon as we got all the pixel, + * or EOF code will not be read at all, and + * DGifGetLine/Pixel clean everything. */ + GifFile->Error = D_GIF_ERR_EOF_TOO_SOON; + return GIF_ERROR; + } else if (CrntCode == ClearCode) { + /* We need to start over again: */ + for (j = 0; j <= LZ_MAX_CODE; j++) { + Prefix[j] = NO_SUCH_CODE; + } + Private->RunningCode = Private->EOFCode + 1; + Private->RunningBits = Private->BitsPerPixel + 1; + Private->MaxCode1 = 1 << Private->RunningBits; + LastCode = Private->LastCode = NO_SUCH_CODE; + } else { + /* Its regular code - if in pixel range simply add it to + * output stream, otherwise trace to codes linked list + * until the prefix is in pixel range: */ + if (CrntCode < ClearCode) { + /* This is simple - its pixel scalar, so add it + * to output: */ + Line[i++] = CrntCode; + } else { + /* Its a code to needed to be traced: trace the + * linked list until the prefix is a pixel, + * while pushing the suffix pixels on our stack. + * If we done, pop the stack in reverse (thats + * what stack is good for!) order to output. */ + if (Prefix[CrntCode] == NO_SUCH_CODE) { + CrntPrefix = LastCode; + + /* Only allowed if CrntCode is exactly + * the running code: In that case + * CrntCode = XXXCode, CrntCode or the + * prefix code is last code and the + * suffix char is exactly the prefix of + * last code! */ + if (CrntCode == + Private->RunningCode - 2) { + Suffix[Private->RunningCode - + 2] = Stack[StackPtr++] = + DGifGetPrefixChar( + Prefix, LastCode, + ClearCode); + } else { + Suffix[Private->RunningCode - + 2] = Stack[StackPtr++] = + DGifGetPrefixChar( + Prefix, CrntCode, + ClearCode); + } + } else { + CrntPrefix = CrntCode; + } + + /* Now (if image is O.K.) we should not get a + * NO_SUCH_CODE during the trace. As we might + * loop forever, in case of defective image, we + * use StackPtr as loop counter and stop before + * overflowing Stack[]. */ + while (StackPtr < LZ_MAX_CODE && + CrntPrefix > ClearCode && + CrntPrefix <= LZ_MAX_CODE) { + Stack[StackPtr++] = Suffix[CrntPrefix]; + CrntPrefix = Prefix[CrntPrefix]; + } + if (StackPtr >= LZ_MAX_CODE || + CrntPrefix > LZ_MAX_CODE) { + GifFile->Error = D_GIF_ERR_IMAGE_DEFECT; + return GIF_ERROR; + } + /* Push the last character on stack: */ + Stack[StackPtr++] = CrntPrefix; + + /* Now lets pop all the stack into output: */ + while (StackPtr != 0 && i < LineLen) { + Line[i++] = Stack[--StackPtr]; + } + } + if (LastCode != NO_SUCH_CODE && + Private->RunningCode - 2 < (LZ_MAX_CODE + 1) && + Prefix[Private->RunningCode - 2] == NO_SUCH_CODE) { + Prefix[Private->RunningCode - 2] = LastCode; + + if (CrntCode == Private->RunningCode - 2) { + /* Only allowed if CrntCode is exactly + * the running code: In that case + * CrntCode = XXXCode, CrntCode or the + * prefix code is last code and the + * suffix char is exactly the prefix of + * last code! */ + Suffix[Private->RunningCode - 2] = + DGifGetPrefixChar(Prefix, LastCode, + ClearCode); + } else { + Suffix[Private->RunningCode - 2] = + DGifGetPrefixChar(Prefix, CrntCode, + ClearCode); + } + } + LastCode = CrntCode; + } + } + + Private->LastCode = LastCode; + Private->StackPtr = StackPtr; + + return GIF_OK; +} + +/****************************************************************************** + Routine to trace the Prefixes linked list until we get a prefix which is + not code, but a pixel value (less than ClearCode). Returns that pixel value. + If image is defective, we might loop here forever, so we limit the loops to + the maximum possible if image O.k. - LZ_MAX_CODE times. +******************************************************************************/ +static int DGifGetPrefixChar(const GifPrefixType *Prefix, int Code, + int ClearCode) { + int i = 0; + + while (Code > ClearCode && i++ <= LZ_MAX_CODE) { + if (Code > LZ_MAX_CODE) { + return NO_SUCH_CODE; + } + Code = Prefix[Code]; + } + return Code; +} + +/****************************************************************************** + Interface for accessing the LZ codes directly. Set Code to the real code + (12bits), or to -1 if EOF code is returned. +******************************************************************************/ +int DGifGetLZCodes(GifFileType *GifFile, int *Code) { + GifByteType *CodeBlock; + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + if (!IS_READABLE(Private)) { + /* This file was NOT open for reading: */ + GifFile->Error = D_GIF_ERR_NOT_READABLE; + return GIF_ERROR; + } + + if (DGifDecompressInput(GifFile, Code) == GIF_ERROR) { + return GIF_ERROR; + } + + if (*Code == Private->EOFCode) { + /* Skip rest of codes (hopefully only NULL terminating block): + */ + do { + if (DGifGetCodeNext(GifFile, &CodeBlock) == GIF_ERROR) { + return GIF_ERROR; + } + } while (CodeBlock != NULL); + + *Code = -1; + } else if (*Code == Private->ClearCode) { + /* We need to start over again: */ + Private->RunningCode = Private->EOFCode + 1; + Private->RunningBits = Private->BitsPerPixel + 1; + Private->MaxCode1 = 1 << Private->RunningBits; + } + + return GIF_OK; +} + +/****************************************************************************** + The LZ decompression input routine: + This routine is responsable for the decompression of the bit stream from + 8 bits (bytes) packets, into the real codes. + Returns GIF_OK if read successfully. +******************************************************************************/ +static int DGifDecompressInput(GifFileType *GifFile, int *Code) { + static const unsigned short CodeMasks[] = { + 0x0000, 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, + 0x007f, 0x00ff, 0x01ff, 0x03ff, 0x07ff, 0x0fff}; + + GifFilePrivateType *Private = (GifFilePrivateType *)GifFile->Private; + + GifByteType NextByte; + + /* The image can't contain more than LZ_BITS per code. */ + if (Private->RunningBits > LZ_BITS) { + GifFile->Error = D_GIF_ERR_IMAGE_DEFECT; + return GIF_ERROR; + } + + while (Private->CrntShiftState < Private->RunningBits) { + /* Needs to get more bytes from input stream for next code: */ + if (DGifBufferedInput(GifFile, Private->Buf, &NextByte) == + GIF_ERROR) { + return GIF_ERROR; + } + Private->CrntShiftDWord |= ((unsigned long)NextByte) + << Private->CrntShiftState; + Private->CrntShiftState += 8; + } + *Code = Private->CrntShiftDWord & CodeMasks[Private->RunningBits]; + + Private->CrntShiftDWord >>= Private->RunningBits; + Private->CrntShiftState -= Private->RunningBits; + + /* If code cannot fit into RunningBits bits, must raise its size. Note + * however that codes above 4095 are used for special signaling. + * If we're using LZ_BITS bits already and we're at the max code, just + * keep using the table as it is, don't increment Private->RunningCode. + */ + if (Private->RunningCode < LZ_MAX_CODE + 2 && + ++Private->RunningCode > Private->MaxCode1 && + Private->RunningBits < LZ_BITS) { + Private->MaxCode1 <<= 1; + Private->RunningBits++; + } + return GIF_OK; +} + +/****************************************************************************** + This routines read one GIF data block at a time and buffers it internally + so that the decompression routine could access it. + The routine returns the next byte from its internal buffer (or read next + block in if buffer empty) and returns GIF_OK if succesful. +******************************************************************************/ +static int DGifBufferedInput(GifFileType *GifFile, GifByteType *Buf, + GifByteType *NextByte) { + if (Buf[0] == 0) { + /* Needs to read the next buffer - this one is empty: */ + /* coverity[check_return] */ + if (InternalRead(GifFile, Buf, 1) != 1) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + /* There shouldn't be any empty data blocks here as the LZW spec + * says the LZW termination code should come first. Therefore + * we shouldn't be inside this routine at that point. + */ + if (Buf[0] == 0) { + GifFile->Error = D_GIF_ERR_IMAGE_DEFECT; + return GIF_ERROR; + } + if (InternalRead(GifFile, &Buf[1], Buf[0]) != Buf[0]) { + GifFile->Error = D_GIF_ERR_READ_FAILED; + return GIF_ERROR; + } + *NextByte = Buf[1]; + Buf[1] = 2; /* We use now the second place as last char read! */ + Buf[0]--; + } else { + *NextByte = Buf[Buf[1]++]; + Buf[0]--; + } + + return GIF_OK; +} + +/****************************************************************************** + This routine is called in case of error during parsing image. We need to + decrease image counter and reallocate memory for saved images. Not decreasing + ImageCount may lead to null pointer dereference, because the last element in + SavedImages may point to the spoilt image and null pointer buffers. +*******************************************************************************/ +void DGifDecreaseImageCounter(GifFileType *GifFile) { + GifFile->ImageCount--; + if (GifFile->SavedImages[GifFile->ImageCount].RasterBits != NULL) { + free(GifFile->SavedImages[GifFile->ImageCount].RasterBits); + } + + // Realloc array according to the new image counter. + SavedImage *correct_saved_images = (SavedImage *)reallocarray( + GifFile->SavedImages, GifFile->ImageCount, sizeof(SavedImage)); + if (correct_saved_images != NULL) { + GifFile->SavedImages = correct_saved_images; + } +} + +/****************************************************************************** + This routine reads an entire GIF into core, hanging all its state info off + the GifFileType pointer. Call DGifOpenFileName() or DGifOpenFileHandle() + first to initialize I/O. Its inverse is EGifSpew(). +*******************************************************************************/ +int DGifSlurp(GifFileType *GifFile) { + size_t ImageSize; + GifRecordType RecordType; + SavedImage *sp; + GifByteType *ExtData; + int ExtFunction; + + GifFile->ExtensionBlocks = NULL; + GifFile->ExtensionBlockCount = 0; + + do { + if (DGifGetRecordType(GifFile, &RecordType) == GIF_ERROR) { + return (GIF_ERROR); + } + + switch (RecordType) { + case IMAGE_DESC_RECORD_TYPE: + if (DGifGetImageDesc(GifFile) == GIF_ERROR) { + return (GIF_ERROR); + } + + sp = &GifFile->SavedImages[GifFile->ImageCount - 1]; + /* Allocate memory for the image */ + if (sp->ImageDesc.Width <= 0 || + sp->ImageDesc.Height <= 0 || + sp->ImageDesc.Width > + (INT_MAX / sp->ImageDesc.Height)) { + DGifDecreaseImageCounter(GifFile); + return GIF_ERROR; + } + ImageSize = sp->ImageDesc.Width * sp->ImageDesc.Height; + + if (ImageSize > (SIZE_MAX / sizeof(GifPixelType))) { + DGifDecreaseImageCounter(GifFile); + return GIF_ERROR; + } + sp->RasterBits = (unsigned char *)reallocarray( + NULL, ImageSize, sizeof(GifPixelType)); + + if (sp->RasterBits == NULL) { + DGifDecreaseImageCounter(GifFile); + return GIF_ERROR; + } + + if (sp->ImageDesc.Interlace) { + int i, j; + /* + * The way an interlaced image should be read - + * offsets and jumps... + */ + static const int InterlacedOffset[] = {0, 4, 2, + 1}; + static const int InterlacedJumps[] = {8, 8, 4, + 2}; + /* Need to perform 4 passes on the image */ + for (i = 0; i < 4; i++) { + for (j = InterlacedOffset[i]; + j < sp->ImageDesc.Height; + j += InterlacedJumps[i]) { + if (DGifGetLine( + GifFile, + sp->RasterBits + + j * sp->ImageDesc + .Width, + sp->ImageDesc.Width) == + GIF_ERROR) { + DGifDecreaseImageCounter( + GifFile); + return GIF_ERROR; + } + } + } + } else { + if (DGifGetLine(GifFile, sp->RasterBits, + ImageSize) == GIF_ERROR) { + DGifDecreaseImageCounter(GifFile); + return GIF_ERROR; + } + } + + if (GifFile->ExtensionBlocks) { + sp->ExtensionBlocks = GifFile->ExtensionBlocks; + sp->ExtensionBlockCount = + GifFile->ExtensionBlockCount; + + GifFile->ExtensionBlocks = NULL; + GifFile->ExtensionBlockCount = 0; + } + break; + + case EXTENSION_RECORD_TYPE: + if (DGifGetExtension(GifFile, &ExtFunction, &ExtData) == + GIF_ERROR) { + return (GIF_ERROR); + } + /* Create an extension block with our data */ + if (ExtData != NULL) { + if (GifAddExtensionBlock( + &GifFile->ExtensionBlockCount, + &GifFile->ExtensionBlocks, ExtFunction, + ExtData[0], &ExtData[1]) == GIF_ERROR) { + return (GIF_ERROR); + } + } + for (;;) { + if (DGifGetExtensionNext(GifFile, &ExtData) == + GIF_ERROR) { + return (GIF_ERROR); + } + if (ExtData == NULL) { + break; + } + /* Continue the extension block */ + if (GifAddExtensionBlock( + &GifFile->ExtensionBlockCount, + &GifFile->ExtensionBlocks, + CONTINUE_EXT_FUNC_CODE, ExtData[0], + &ExtData[1]) == GIF_ERROR) { + return (GIF_ERROR); + } + } + break; + + case TERMINATE_RECORD_TYPE: + break; + + default: /* Should be trapped by DGifGetRecordType */ + break; + } + } while (RecordType != TERMINATE_RECORD_TYPE); + + /* Sanity check for corrupted file */ + if (GifFile->ImageCount == 0) { + GifFile->Error = D_GIF_ERR_NO_IMAG_DSCR; + return (GIF_ERROR); + } + + return (GIF_OK); +} + +/* end */ diff --git a/native/vendor/giflib/gif_hash.c b/native/vendor/giflib/gif_hash.c new file mode 100644 index 0000000..42efbe8 --- /dev/null +++ b/native/vendor/giflib/gif_hash.c @@ -0,0 +1,129 @@ +// @nolint (improperly imported third-party code) +/***************************************************************************** + +gif_hash.c -- module to support the following operations: + +1. InitHashTable - initialize hash table. +2. ClearHashTable - clear the hash table to an empty state. +2. InsertHashTable - insert one item into data structure. +3. ExistsHashTable - test if item exists in data structure. + +This module is used to hash the GIF codes during encoding. + +*****************************************************************************/ +// SPDX-License-Identifier: MIT +// SPDX-File-Copyright-Txt: (C) Copyright 1989 Gershon Elber + +#include +#include +#include +#include +#include + +#include "gif_hash.h" +#include "gif_lib.h" +#include "gif_lib_private.h" + +/* #define DEBUG_HIT_RATE Debug number of misses per hash Insert/Exists. */ + +#ifdef DEBUG_HIT_RATE +static long NumberOfTests = 0, NumberOfMisses = 0; +#endif /* DEBUG_HIT_RATE */ + +static int KeyItem(uint32_t Item); + +/****************************************************************************** + Initialize HashTable - allocate the memory needed and clear it. * +******************************************************************************/ +GifHashTableType *_InitHashTable(void) { + GifHashTableType *HashTable; + + if ((HashTable = (GifHashTableType *)malloc( + sizeof(GifHashTableType))) == NULL) { + return NULL; + } + + _ClearHashTable(HashTable); + + return HashTable; +} + +/****************************************************************************** + Routine to clear the HashTable to an empty state. * + This part is a little machine depended. Use the commented part otherwise. * +******************************************************************************/ +void _ClearHashTable(GifHashTableType *HashTable) { + memset(HashTable->HTable, 0xFF, HT_SIZE * sizeof(uint32_t)); +} + +/****************************************************************************** + Routine to insert a new Item into the HashTable. The data is assumed to be * + new one. * +******************************************************************************/ +void _InsertHashTable(GifHashTableType *HashTable, uint32_t Key, int Code) { + int HKey = KeyItem(Key); + uint32_t *HTable = HashTable->HTable; + +#ifdef DEBUG_HIT_RATE + NumberOfTests++; + NumberOfMisses++; +#endif /* DEBUG_HIT_RATE */ + + while (HT_GET_KEY(HTable[HKey]) != 0xFFFFFL) { +#ifdef DEBUG_HIT_RATE + NumberOfMisses++; +#endif /* DEBUG_HIT_RATE */ + HKey = (HKey + 1) & HT_KEY_MASK; + } + HTable[HKey] = HT_PUT_KEY(Key) | HT_PUT_CODE(Code); +} + +/****************************************************************************** + Routine to test if given Key exists in HashTable and if so returns its code * + Returns the Code if key was found, -1 if not. * +******************************************************************************/ +int _ExistsHashTable(GifHashTableType *HashTable, uint32_t Key) { + int HKey = KeyItem(Key); + uint32_t *HTable = HashTable->HTable, HTKey; + +#ifdef DEBUG_HIT_RATE + NumberOfTests++; + NumberOfMisses++; +#endif /* DEBUG_HIT_RATE */ + + while ((HTKey = HT_GET_KEY(HTable[HKey])) != 0xFFFFFL) { +#ifdef DEBUG_HIT_RATE + NumberOfMisses++; +#endif /* DEBUG_HIT_RATE */ + if (Key == HTKey) { + return HT_GET_CODE(HTable[HKey]); + } + HKey = (HKey + 1) & HT_KEY_MASK; + } + + return -1; +} + +/****************************************************************************** + Routine to generate an HKey for the hashtable out of the given unique key. * + The given Key is assumed to be 20 bits as follows: lower 8 bits are the * + new postfix character, while the upper 12 bits are the prefix code. * + Because the average hit ratio is only 2 (2 hash references per entry), * + evaluating more complex keys (such as twin prime keys) does not worth it! * +******************************************************************************/ +static int KeyItem(uint32_t Item) { + return ((Item >> 12) ^ Item) & HT_KEY_MASK; +} + +#ifdef DEBUG_HIT_RATE +/****************************************************************************** + Debugging routine to print the hit ratio - number of times the hash table * + was tested per operation. This routine was used to test the KeyItem routine * +******************************************************************************/ +void HashTablePrintHitRatio(void) { + printf("Hash Table Hit Ratio is %ld/%ld = %ld%%.\n", NumberOfMisses, + NumberOfTests, NumberOfMisses * 100 / NumberOfTests); +} +#endif /* DEBUG_HIT_RATE */ + +/* end */ diff --git a/native/vendor/giflib/gif_hash.h b/native/vendor/giflib/gif_hash.h new file mode 100644 index 0000000..3066fb1 --- /dev/null +++ b/native/vendor/giflib/gif_hash.h @@ -0,0 +1,43 @@ +// @nolint (improperly imported third-party code) +/****************************************************************************** + +gif_hash.h - magfic constants and declarations for GIF LZW + +******************************************************************************/ +// SPDX-License-Identifier: MIT + +#ifndef _GIF_HASH_H_ +#define _GIF_HASH_H_ + +#ifndef _WIN32 +#include +#endif /* _WIN32 */ +#include + +#define HT_SIZE 8192 /* 12bits = 4096 or twice as big! */ +#define HT_KEY_MASK 0x1FFF /* 13bits keys */ +#define HT_KEY_NUM_BITS 13 /* 13bits keys */ +#define HT_MAX_KEY 8191 /* 13bits - 1, maximal code possible */ +#define HT_MAX_CODE 4095 /* Biggest code possible in 12 bits. */ + +/* The 32 bits of the long are divided into two parts for the key & code: */ +/* 1. The code is 12 bits as our compression algorithm is limited to 12bits */ +/* 2. The key is 12 bits Prefix code + 8 bit new char or 20 bits. */ +/* The key is the upper 20 bits. The code is the lower 12. */ +#define HT_GET_KEY(l) (l >> 12) +#define HT_GET_CODE(l) (l & 0x0FFF) +#define HT_PUT_KEY(l) (l << 12) +#define HT_PUT_CODE(l) (l & 0x0FFF) + +typedef struct GifHashTableType { + uint32_t HTable[HT_SIZE]; +} GifHashTableType; + +GifHashTableType *_InitHashTable(void); +void _ClearHashTable(GifHashTableType *HashTable); +void _InsertHashTable(GifHashTableType *HashTable, uint32_t Key, int Code); +int _ExistsHashTable(GifHashTableType *HashTable, uint32_t Key); + +#endif /* _GIF_HASH_H_ */ + +/* end */ diff --git a/native/vendor/giflib/gif_lib.h b/native/vendor/giflib/gif_lib.h new file mode 100644 index 0000000..7bed043 --- /dev/null +++ b/native/vendor/giflib/gif_lib.h @@ -0,0 +1,292 @@ +// @nolint (improperly imported third-party code) +/****************************************************************************** + +gif_lib.h - service library for decoding and encoding GIF images + +SPDX-License-Identifier: MIT + +*****************************************************************************/ + +#ifndef _GIF_LIB_H_ +#define _GIF_LIB_H_ 1 + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +#define GIFLIB_MAJOR 5 +#define GIFLIB_MINOR 2 +#define GIFLIB_RELEASE 2 + +#define GIF_ERROR 0 +#define GIF_OK 1 + +#include +#include + +#define GIF_STAMP "GIFVER" /* First chars in file - GIF stamp. */ +#define GIF_STAMP_LEN sizeof(GIF_STAMP) - 1 +#define GIF_VERSION_POS 3 /* Version first character in stamp. */ +#define GIF87_STAMP "GIF87a" /* First chars in file - GIF stamp. */ +#define GIF89_STAMP "GIF89a" /* First chars in file - GIF stamp. */ + +typedef unsigned char GifPixelType; +typedef unsigned char *GifRowType; +typedef unsigned char GifByteType; +typedef unsigned int GifPrefixType; +typedef int GifWord; + +typedef struct GifColorType { + GifByteType Red, Green, Blue; +} GifColorType; + +typedef struct ColorMapObject { + int ColorCount; + int BitsPerPixel; + bool SortFlag; + GifColorType *Colors; /* on malloc(3) heap */ +} ColorMapObject; + +typedef struct GifImageDesc { + GifWord Left, Top, Width, Height; /* Current image dimensions. */ + bool Interlace; /* Sequential/Interlaced lines. */ + ColorMapObject *ColorMap; /* The local color map */ +} GifImageDesc; + +typedef struct ExtensionBlock { + int ByteCount; + GifByteType *Bytes; /* on malloc(3) heap */ + int Function; /* The block function code */ +#define CONTINUE_EXT_FUNC_CODE 0x00 /* continuation subblock */ +#define COMMENT_EXT_FUNC_CODE 0xfe /* comment */ +#define GRAPHICS_EXT_FUNC_CODE 0xf9 /* graphics control (GIF89) */ +#define PLAINTEXT_EXT_FUNC_CODE 0x01 /* plaintext */ +#define APPLICATION_EXT_FUNC_CODE 0xff /* application block (GIF89) */ +} ExtensionBlock; + +typedef struct SavedImage { + GifImageDesc ImageDesc; + GifByteType *RasterBits; /* on malloc(3) heap */ + int ExtensionBlockCount; /* Count of extensions before image */ + ExtensionBlock *ExtensionBlocks; /* Extensions before image */ +} SavedImage; + +typedef struct GifFileType { + GifWord SWidth, SHeight; /* Size of virtual canvas */ + GifWord SColorResolution; /* How many colors can we generate? */ + GifWord SBackGroundColor; /* Background color for virtual canvas */ + GifByteType AspectByte; /* Used to compute pixel aspect ratio */ + ColorMapObject *SColorMap; /* Global colormap, NULL if nonexistent. */ + int ImageCount; /* Number of current image (both APIs) */ + GifImageDesc Image; /* Current image (low-level API) */ + SavedImage *SavedImages; /* Image sequence (high-level API) */ + int ExtensionBlockCount; /* Count extensions past last image */ + ExtensionBlock *ExtensionBlocks; /* Extensions past last image */ + int Error; /* Last error condition reported */ + void *UserData; /* hook to attach user data (TVT) */ + void *Private; /* Don't mess with this! */ +} GifFileType; + +#define GIF_ASPECT_RATIO(n) ((n) + 15.0 / 64.0) + +typedef enum { + UNDEFINED_RECORD_TYPE, + SCREEN_DESC_RECORD_TYPE, + IMAGE_DESC_RECORD_TYPE, /* Begin with ',' */ + EXTENSION_RECORD_TYPE, /* Begin with '!' */ + TERMINATE_RECORD_TYPE /* Begin with ';' */ +} GifRecordType; + +/* func type to read gif data from arbitrary sources (TVT) */ +typedef int (*InputFunc)(GifFileType *, GifByteType *, int); + +/* func type to write gif data to arbitrary targets. + * Returns count of bytes written. (MRB) + */ +typedef int (*OutputFunc)(GifFileType *, const GifByteType *, int); + +/****************************************************************************** + GIF89 structures +******************************************************************************/ + +typedef struct GraphicsControlBlock { + int DisposalMode; +#define DISPOSAL_UNSPECIFIED 0 /* No disposal specified. */ +#define DISPOSE_DO_NOT 1 /* Leave image in place */ +#define DISPOSE_BACKGROUND 2 /* Set area too background color */ +#define DISPOSE_PREVIOUS 3 /* Restore to previous content */ + bool UserInputFlag; /* User confirmation required before disposal */ + int DelayTime; /* pre-display delay in 0.01sec units */ + int TransparentColor; /* Palette index for transparency, -1 if none */ +#define NO_TRANSPARENT_COLOR -1 +} GraphicsControlBlock; + +/****************************************************************************** + GIF encoding routines +******************************************************************************/ + +/* Main entry points */ +GifFileType *EGifOpenFileName(const char *GifFileName, + const bool GifTestExistence, int *Error); +GifFileType *EGifOpenFileHandle(const int GifFileHandle, int *Error); +GifFileType *EGifOpen(void *userPtr, OutputFunc writeFunc, int *Error); +int EGifSpew(GifFileType *GifFile); +const char *EGifGetGifVersion(GifFileType *GifFile); /* new in 5.x */ +int EGifCloseFile(GifFileType *GifFile, int *ErrorCode); + +#define E_GIF_SUCCEEDED 0 +#define E_GIF_ERR_OPEN_FAILED 1 /* And EGif possible errors. */ +#define E_GIF_ERR_WRITE_FAILED 2 +#define E_GIF_ERR_HAS_SCRN_DSCR 3 +#define E_GIF_ERR_HAS_IMAG_DSCR 4 +#define E_GIF_ERR_NO_COLOR_MAP 5 +#define E_GIF_ERR_DATA_TOO_BIG 6 +#define E_GIF_ERR_NOT_ENOUGH_MEM 7 +#define E_GIF_ERR_DISK_IS_FULL 8 +#define E_GIF_ERR_CLOSE_FAILED 9 +#define E_GIF_ERR_NOT_WRITEABLE 10 + +/* These are legacy. You probably do not want to call them directly */ +int EGifPutScreenDesc(GifFileType *GifFile, const int GifWidth, + const int GifHeight, const int GifColorRes, + const int GifBackGround, + const ColorMapObject *GifColorMap); +int EGifPutImageDesc(GifFileType *GifFile, const int GifLeft, const int GifTop, + const int GifWidth, const int GifHeight, + const bool GifInterlace, + const ColorMapObject *GifColorMap); +void EGifSetGifVersion(GifFileType *GifFile, const bool gif89); +int EGifPutLine(GifFileType *GifFile, GifPixelType *GifLine, int GifLineLen); +int EGifPutPixel(GifFileType *GifFile, const GifPixelType GifPixel); +int EGifPutComment(GifFileType *GifFile, const char *GifComment); +int EGifPutExtensionLeader(GifFileType *GifFile, const int GifExtCode); +int EGifPutExtensionBlock(GifFileType *GifFile, const int GifExtLen, + const void *GifExtension); +int EGifPutExtensionTrailer(GifFileType *GifFile); +int EGifPutExtension(GifFileType *GifFile, const int GifExtCode, + const int GifExtLen, const void *GifExtension); +int EGifPutCode(GifFileType *GifFile, int GifCodeSize, + const GifByteType *GifCodeBlock); +int EGifPutCodeNext(GifFileType *GifFile, const GifByteType *GifCodeBlock); + +/****************************************************************************** + GIF decoding routines +******************************************************************************/ + +/* Main entry points */ +GifFileType *DGifOpenFileName(const char *GifFileName, int *Error); +GifFileType *DGifOpenFileHandle(int GifFileHandle, int *Error); +int DGifSlurp(GifFileType *GifFile); +GifFileType *DGifOpen(void *userPtr, InputFunc readFunc, + int *Error); /* new one (TVT) */ +int DGifCloseFile(GifFileType *GifFile, int *ErrorCode); + +#define D_GIF_SUCCEEDED 0 +#define D_GIF_ERR_OPEN_FAILED 101 /* And DGif possible errors. */ +#define D_GIF_ERR_READ_FAILED 102 +#define D_GIF_ERR_NOT_GIF_FILE 103 +#define D_GIF_ERR_NO_SCRN_DSCR 104 +#define D_GIF_ERR_NO_IMAG_DSCR 105 +#define D_GIF_ERR_NO_COLOR_MAP 106 +#define D_GIF_ERR_WRONG_RECORD 107 +#define D_GIF_ERR_DATA_TOO_BIG 108 +#define D_GIF_ERR_NOT_ENOUGH_MEM 109 +#define D_GIF_ERR_CLOSE_FAILED 110 +#define D_GIF_ERR_NOT_READABLE 111 +#define D_GIF_ERR_IMAGE_DEFECT 112 +#define D_GIF_ERR_EOF_TOO_SOON 113 + +/* These are legacy. You probably do not want to call them directly */ +int DGifGetScreenDesc(GifFileType *GifFile); +int DGifGetRecordType(GifFileType *GifFile, GifRecordType *GifType); +int DGifGetImageHeader(GifFileType *GifFile); +int DGifGetImageDesc(GifFileType *GifFile); +int DGifGetLine(GifFileType *GifFile, GifPixelType *GifLine, int GifLineLen); +int DGifGetPixel(GifFileType *GifFile, GifPixelType GifPixel); +int DGifGetExtension(GifFileType *GifFile, int *GifExtCode, + GifByteType **GifExtension); +int DGifGetExtensionNext(GifFileType *GifFile, GifByteType **GifExtension); +int DGifGetCode(GifFileType *GifFile, int *GifCodeSize, + GifByteType **GifCodeBlock); +int DGifGetCodeNext(GifFileType *GifFile, GifByteType **GifCodeBlock); +int DGifGetLZCodes(GifFileType *GifFile, int *GifCode); +const char *DGifGetGifVersion(GifFileType *GifFile); + +/****************************************************************************** + Error handling and reporting. +******************************************************************************/ +extern const char *GifErrorString(int ErrorCode); /* new in 2012 - ESR */ + +/***************************************************************************** + it g in core. +******************************************************************************/ + +/****************************************************************************** + Color map handling from gif_alloc.c +******************************************************************************/ + +extern ColorMapObject *GifMakeMapObject(int ColorCount, + const GifColorType *ColorMap); +extern void GifFreeMapObject(ColorMapObject *Object); +extern ColorMapObject *GifUnionColorMap(const ColorMapObject *ColorIn1, + const ColorMapObject *ColorIn2, + GifPixelType ColorTransIn2[]); +extern int GifBitSize(int n); + +/****************************************************************************** + Support for the in-core structures allocation (slurp mode). +******************************************************************************/ + +extern void GifApplyTranslation(SavedImage *Image, + const GifPixelType Translation[]); +extern int GifAddExtensionBlock(int *ExtensionBlock_Count, + ExtensionBlock **ExtensionBlocks, int Function, + unsigned int Len, unsigned char ExtData[]); +extern void GifFreeExtensions(int *ExtensionBlock_Count, + ExtensionBlock **ExtensionBlocks); +extern SavedImage *GifMakeSavedImage(GifFileType *GifFile, + const SavedImage *CopyFrom); +extern void GifFreeSavedImages(GifFileType *GifFile); + +/****************************************************************************** + 5.x functions for GIF89 graphics control blocks +******************************************************************************/ + +int DGifExtensionToGCB(const size_t GifExtensionLength, + const GifByteType *GifExtension, + GraphicsControlBlock *GCB); +size_t EGifGCBToExtension(const GraphicsControlBlock *GCB, + GifByteType *GifExtension); + +int DGifSavedExtensionToGCB(GifFileType *GifFile, int ImageIndex, + GraphicsControlBlock *GCB); +int EGifGCBToSavedExtension(const GraphicsControlBlock *GCB, + GifFileType *GifFile, int ImageIndex); + +/****************************************************************************** + The library's internal utility font +******************************************************************************/ + +#define GIF_FONT_WIDTH 8 +#define GIF_FONT_HEIGHT 8 +extern const unsigned char GifAsciiTable8x8[][GIF_FONT_WIDTH]; + +extern void GifDrawText8x8(SavedImage *Image, const int x, const int y, + const char *legend, const int color); + +extern void GifDrawBox(SavedImage *Image, const int x, const int y, const int w, + const int d, const int color); + +extern void GifDrawRectangle(SavedImage *Image, const int x, const int y, + const int w, const int d, const int color); + +extern void GifDrawBoxedText8x8(SavedImage *Image, const int x, const int y, + const char *legend, const int border, + const int bg, const int fg); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* _GIF_LIB_H */ + +/* end */ diff --git a/native/vendor/giflib/gif_lib_private.h b/native/vendor/giflib/gif_lib_private.h new file mode 100644 index 0000000..0498715 --- /dev/null +++ b/native/vendor/giflib/gif_lib_private.h @@ -0,0 +1,73 @@ +// @nolint (improperly imported third-party code) +/**************************************************************************** + +gif_lib_private.h - internal giflib routines and structures + +SPDX-License-Identifier: MIT + +****************************************************************************/ + +#ifndef _GIF_LIB_PRIVATE_H +#define _GIF_LIB_PRIVATE_H + +#include "gif_hash.h" +#include "gif_lib.h" + +#ifndef SIZE_MAX +#define SIZE_MAX UINTPTR_MAX +#endif + +#define EXTENSION_INTRODUCER 0x21 +#define DESCRIPTOR_INTRODUCER 0x2c +#define TERMINATOR_INTRODUCER 0x3b + +#define LZ_MAX_CODE 4095 /* Biggest code possible in 12 bits. */ +#define LZ_BITS 12 + +#define FLUSH_OUTPUT 4096 /* Impossible code, to signal flush. */ +#define FIRST_CODE 4097 /* Impossible code, to signal first. */ +#define NO_SUCH_CODE 4098 /* Impossible code, to signal empty. */ + +#define FILE_STATE_WRITE 0x01 +#define FILE_STATE_SCREEN 0x02 +#define FILE_STATE_IMAGE 0x04 +#define FILE_STATE_READ 0x08 + +#define IS_READABLE(Private) (Private->FileState & FILE_STATE_READ) +#define IS_WRITEABLE(Private) (Private->FileState & FILE_STATE_WRITE) + +typedef struct GifFilePrivateType { + GifWord FileState, FileHandle, /* Where all this data goes to! */ + BitsPerPixel, /* Bits per pixel (Codes uses at least this + 1). */ + ClearCode, /* The CLEAR LZ code. */ + EOFCode, /* The EOF LZ code. */ + RunningCode, /* The next code algorithm can generate. */ + RunningBits, /* The number of bits required to represent + RunningCode. */ + MaxCode1, /* 1 bigger than max. possible code, in RunningBits bits. + */ + LastCode, /* The code before the current code. */ + CrntCode, /* Current algorithm code. */ + StackPtr, /* For character stack (see below). */ + CrntShiftState; /* Number of bits in CrntShiftDWord. */ + unsigned long CrntShiftDWord; /* For bytes decomposition into codes. */ + unsigned long PixelCount; /* Number of pixels in image. */ + FILE *File; /* File as stream. */ + InputFunc Read; /* function to read gif input (TVT) */ + OutputFunc Write; /* function to write gif output (MRB) */ + GifByteType Buf[256]; /* Compressed input is buffered here. */ + GifByteType Stack[LZ_MAX_CODE]; /* Decoded pixels are stacked here. */ + GifByteType Suffix[LZ_MAX_CODE + 1]; /* So we can trace the codes. */ + GifPrefixType Prefix[LZ_MAX_CODE + 1]; + GifHashTableType *HashTable; + bool gif89; +} GifFilePrivateType; + +#ifndef HAVE_REALLOCARRAY +extern void *openbsd_reallocarray(void *optr, size_t nmemb, size_t size); +#define reallocarray openbsd_reallocarray +#endif + +#endif /* _GIF_LIB_PRIVATE_H */ + +/* end */ diff --git a/native/vendor/giflib/gifalloc.c b/native/vendor/giflib/gifalloc.c new file mode 100644 index 0000000..65679d2 --- /dev/null +++ b/native/vendor/giflib/gifalloc.c @@ -0,0 +1,426 @@ +// @nolint (improperly imported third-party code) +/***************************************************************************** + + GIF construction tools + +****************************************************************************/ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: Copyright (C) Eric S. Raymond + +#include +#include +#include + +#include "gif_lib.h" +#include "gif_lib_private.h" + +#define MAX(x, y) (((x) > (y)) ? (x) : (y)) + +/****************************************************************************** + Miscellaneous utility functions +******************************************************************************/ + +/* return smallest bitfield size n will fit in */ +int GifBitSize(int n) { + int i; + + for (i = 1; i <= 8; i++) { + if ((1 << i) >= n) { + break; + } + } + return (i); +} + +/****************************************************************************** + Color map object functions +******************************************************************************/ + +/* + * Allocate a color map of given size; initialize with contents of + * ColorMap if that pointer is non-NULL. + */ +ColorMapObject *GifMakeMapObject(int ColorCount, const GifColorType *ColorMap) { + ColorMapObject *Object; + + /*** FIXME: Our ColorCount has to be a power of two. Is it necessary to + * make the user know that or should we automatically round up instead? + */ + if (ColorCount != (1 << GifBitSize(ColorCount))) { + return ((ColorMapObject *)NULL); + } + + Object = (ColorMapObject *)malloc(sizeof(ColorMapObject)); + if (Object == (ColorMapObject *)NULL) { + return ((ColorMapObject *)NULL); + } + + Object->Colors = + (GifColorType *)calloc(ColorCount, sizeof(GifColorType)); + if (Object->Colors == (GifColorType *)NULL) { + free(Object); + return ((ColorMapObject *)NULL); + } + + Object->ColorCount = ColorCount; + Object->BitsPerPixel = GifBitSize(ColorCount); + Object->SortFlag = false; + + if (ColorMap != NULL) { + memcpy((char *)Object->Colors, (char *)ColorMap, + ColorCount * sizeof(GifColorType)); + } + + return (Object); +} + +/******************************************************************************* + Free a color map object +*******************************************************************************/ +void GifFreeMapObject(ColorMapObject *Object) { + if (Object != NULL) { + (void)free(Object->Colors); + (void)free(Object); + } +} + +#ifdef DEBUG +void DumpColorMap(ColorMapObject *Object, FILE *fp) { + if (Object != NULL) { + int i, j, Len = Object->ColorCount; + + for (i = 0; i < Len; i += 4) { + for (j = 0; j < 4 && j < Len; j++) { + (void)fprintf(fp, "%3d: %02x %02x %02x ", + i + j, Object->Colors[i + j].Red, + Object->Colors[i + j].Green, + Object->Colors[i + j].Blue); + } + (void)fprintf(fp, "\n"); + } + } +} +#endif /* DEBUG */ + +/******************************************************************************* + Compute the union of two given color maps and return it. If result can't + fit into 256 colors, NULL is returned, the allocated union otherwise. + ColorIn1 is copied as is to ColorUnion, while colors from ColorIn2 are + copied iff they didn't exist before. ColorTransIn2 maps the old + ColorIn2 into the ColorUnion color map table./ +*******************************************************************************/ +ColorMapObject *GifUnionColorMap(const ColorMapObject *ColorIn1, + const ColorMapObject *ColorIn2, + GifPixelType ColorTransIn2[]) { + int i, j, CrntSlot, RoundUpTo, NewGifBitSize; + ColorMapObject *ColorUnion; + + /* + * We don't worry about duplicates within either color map; if + * the caller wants to resolve those, he can perform unions + * with an empty color map. + */ + + /* Allocate table which will hold the result for sure. */ + ColorUnion = GifMakeMapObject( + MAX(ColorIn1->ColorCount, ColorIn2->ColorCount) * 2, NULL); + + if (ColorUnion == NULL) { + return (NULL); + } + + /* + * Copy ColorIn1 to ColorUnion. + */ + for (i = 0; i < ColorIn1->ColorCount; i++) { + ColorUnion->Colors[i] = ColorIn1->Colors[i]; + } + CrntSlot = ColorIn1->ColorCount; + + /* + * Potentially obnoxious hack: + * + * Back CrntSlot down past all contiguous {0, 0, 0} slots at the end + * of table 1. This is very useful if your display is limited to + * 16 colors. + */ + while (ColorIn1->Colors[CrntSlot - 1].Red == 0 && + ColorIn1->Colors[CrntSlot - 1].Green == 0 && + ColorIn1->Colors[CrntSlot - 1].Blue == 0) { + CrntSlot--; + } + + /* Copy ColorIn2 to ColorUnion (use old colors if they exist): */ + for (i = 0; i < ColorIn2->ColorCount && CrntSlot <= 256; i++) { + /* Let's see if this color already exists: */ + for (j = 0; j < ColorIn1->ColorCount; j++) { + if (memcmp(&ColorIn1->Colors[j], &ColorIn2->Colors[i], + sizeof(GifColorType)) == 0) { + break; + } + } + + if (j < ColorIn1->ColorCount) { + ColorTransIn2[i] = j; /* color exists in Color1 */ + } else { + /* Color is new - copy it to a new slot: */ + ColorUnion->Colors[CrntSlot] = ColorIn2->Colors[i]; + ColorTransIn2[i] = CrntSlot++; + } + } + + if (CrntSlot > 256) { + GifFreeMapObject(ColorUnion); + return ((ColorMapObject *)NULL); + } + + NewGifBitSize = GifBitSize(CrntSlot); + RoundUpTo = (1 << NewGifBitSize); + + if (RoundUpTo != ColorUnion->ColorCount) { + GifColorType *Map = ColorUnion->Colors; + + /* + * Zero out slots up to next power of 2. + * We know these slots exist because of the way ColorUnion's + * start dimension was computed. + */ + for (j = CrntSlot; j < RoundUpTo; j++) { + Map[j].Red = Map[j].Green = Map[j].Blue = 0; + } + + /* perhaps we can shrink the map? */ + if (RoundUpTo < ColorUnion->ColorCount) { + GifColorType *new_map = (GifColorType *)reallocarray( + Map, RoundUpTo, sizeof(GifColorType)); + if (new_map == NULL) { + GifFreeMapObject(ColorUnion); + return ((ColorMapObject *)NULL); + } + ColorUnion->Colors = new_map; + } + } + + ColorUnion->ColorCount = RoundUpTo; + ColorUnion->BitsPerPixel = NewGifBitSize; + + return (ColorUnion); +} + +/******************************************************************************* + Apply a given color translation to the raster bits of an image +*******************************************************************************/ +void GifApplyTranslation(SavedImage *Image, const GifPixelType Translation[]) { + int i; + int RasterSize = + Image->ImageDesc.Height * Image->ImageDesc.Width; + + for (i = 0; i < RasterSize; i++) { + Image->RasterBits[i] = Translation[Image->RasterBits[i]]; + } +} + +/****************************************************************************** + Extension record functions +******************************************************************************/ +int GifAddExtensionBlock(int *ExtensionBlockCount, + ExtensionBlock **ExtensionBlocks, int Function, + unsigned int Len, unsigned char ExtData[]) { + ExtensionBlock *ep; + + if (*ExtensionBlocks == NULL) { + *ExtensionBlocks = + (ExtensionBlock *)malloc(sizeof(ExtensionBlock)); + } else { + ExtensionBlock *ep_new = (ExtensionBlock *)reallocarray( + *ExtensionBlocks, (*ExtensionBlockCount + 1), + sizeof(ExtensionBlock)); + if (ep_new == NULL) { + return (GIF_ERROR); + } + *ExtensionBlocks = ep_new; + } + + if (*ExtensionBlocks == NULL) { + return (GIF_ERROR); + } + + ep = &(*ExtensionBlocks)[(*ExtensionBlockCount)++]; + + ep->Function = Function; + ep->ByteCount = Len; + ep->Bytes = (GifByteType *)malloc(ep->ByteCount); + if (ep->Bytes == NULL) { + return (GIF_ERROR); + } + + if (ExtData != NULL) { + memcpy(ep->Bytes, ExtData, Len); + } + + return (GIF_OK); +} + +void GifFreeExtensions(int *ExtensionBlockCount, + ExtensionBlock **ExtensionBlocks) { + ExtensionBlock *ep; + + if (*ExtensionBlocks == NULL) { + return; + } + + for (ep = *ExtensionBlocks; + ep < (*ExtensionBlocks + *ExtensionBlockCount); ep++) { + (void)free((char *)ep->Bytes); + } + (void)free((char *)*ExtensionBlocks); + *ExtensionBlocks = NULL; + *ExtensionBlockCount = 0; +} + +/****************************************************************************** + Image block allocation functions +******************************************************************************/ + +/* Private Function: + * Frees the last image in the GifFile->SavedImages array + */ +void FreeLastSavedImage(GifFileType *GifFile) { + SavedImage *sp; + + if ((GifFile == NULL) || (GifFile->SavedImages == NULL)) { + return; + } + + /* Remove one SavedImage from the GifFile */ + GifFile->ImageCount--; + sp = &GifFile->SavedImages[GifFile->ImageCount]; + + /* Deallocate its Colormap */ + if (sp->ImageDesc.ColorMap != NULL) { + GifFreeMapObject(sp->ImageDesc.ColorMap); + sp->ImageDesc.ColorMap = NULL; + } + + /* Deallocate the image data */ + if (sp->RasterBits != NULL) { + free((char *)sp->RasterBits); + } + + /* Deallocate any extensions */ + GifFreeExtensions(&sp->ExtensionBlockCount, &sp->ExtensionBlocks); + + /*** FIXME: We could realloc the GifFile->SavedImages structure but is + * there a point to it? Saves some memory but we'd have to do it every + * time. If this is used in GifFreeSavedImages then it would be + * inefficient (The whole array is going to be deallocated.) If we just + * use it when we want to free the last Image it's convenient to do it + * here. + */ +} + +/* + * Append an image block to the SavedImages array + */ +SavedImage *GifMakeSavedImage(GifFileType *GifFile, + const SavedImage *CopyFrom) { + // cppcheck-suppress ctunullpointer + if (GifFile->SavedImages == NULL) { + GifFile->SavedImages = (SavedImage *)malloc(sizeof(SavedImage)); + } else { + SavedImage *newSavedImages = (SavedImage *)reallocarray( + GifFile->SavedImages, (GifFile->ImageCount + 1), + sizeof(SavedImage)); + if (newSavedImages == NULL) { + return ((SavedImage *)NULL); + } + GifFile->SavedImages = newSavedImages; + } + if (GifFile->SavedImages == NULL) { + return ((SavedImage *)NULL); + } else { + SavedImage *sp = &GifFile->SavedImages[GifFile->ImageCount++]; + + if (CopyFrom != NULL) { + memcpy((char *)sp, CopyFrom, sizeof(SavedImage)); + + /* + * Make our own allocated copies of the heap fields in + * the copied record. This guards against potential + * aliasing problems. + */ + + /* first, the local color map */ + if (CopyFrom->ImageDesc.ColorMap != NULL) { + sp->ImageDesc.ColorMap = GifMakeMapObject( + CopyFrom->ImageDesc.ColorMap->ColorCount, + CopyFrom->ImageDesc.ColorMap->Colors); + if (sp->ImageDesc.ColorMap == NULL) { + FreeLastSavedImage(GifFile); + return (SavedImage *)(NULL); + } + } + + /* next, the raster */ + sp->RasterBits = (unsigned char *)reallocarray( + NULL, + (CopyFrom->ImageDesc.Height * + CopyFrom->ImageDesc.Width), + sizeof(GifPixelType)); + if (sp->RasterBits == NULL) { + FreeLastSavedImage(GifFile); + return (SavedImage *)(NULL); + } + memcpy(sp->RasterBits, CopyFrom->RasterBits, + sizeof(GifPixelType) * + CopyFrom->ImageDesc.Height * + CopyFrom->ImageDesc.Width); + + /* finally, the extension blocks */ + if (CopyFrom->ExtensionBlocks != NULL) { + sp->ExtensionBlocks = + (ExtensionBlock *)reallocarray( + NULL, CopyFrom->ExtensionBlockCount, + sizeof(ExtensionBlock)); + if (sp->ExtensionBlocks == NULL) { + FreeLastSavedImage(GifFile); + return (SavedImage *)(NULL); + } + memcpy(sp->ExtensionBlocks, + CopyFrom->ExtensionBlocks, + sizeof(ExtensionBlock) * + CopyFrom->ExtensionBlockCount); + } + } else { + memset((char *)sp, '\0', sizeof(SavedImage)); + } + + return (sp); + } +} + +void GifFreeSavedImages(GifFileType *GifFile) { + SavedImage *sp; + + if ((GifFile == NULL) || (GifFile->SavedImages == NULL)) { + return; + } + for (sp = GifFile->SavedImages; + sp < GifFile->SavedImages + GifFile->ImageCount; sp++) { + if (sp->ImageDesc.ColorMap != NULL) { + GifFreeMapObject(sp->ImageDesc.ColorMap); + sp->ImageDesc.ColorMap = NULL; + } + + if (sp->RasterBits != NULL) { + free((char *)sp->RasterBits); + } + + GifFreeExtensions(&sp->ExtensionBlockCount, + &sp->ExtensionBlocks); + } + free((char *)GifFile->SavedImages); + GifFile->SavedImages = NULL; +} + +/* end */ diff --git a/native/vendor/giflib/openbsd-reallocarray.c b/native/vendor/giflib/openbsd-reallocarray.c new file mode 100644 index 0000000..7d5f1e7 --- /dev/null +++ b/native/vendor/giflib/openbsd-reallocarray.c @@ -0,0 +1,74 @@ +// @nolint (improperly imported third-party code) +/* + * SPDX-FileCopyrightText: Copyright (C) 2008 Otto Moerbeek + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#ifndef SIZE_MAX +#define SIZE_MAX UINTPTR_MAX +#endif + +/* + * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX + * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW + */ +#define MUL_NO_OVERFLOW ((size_t)1 << (sizeof(size_t) * 4)) + +void *openbsd_reallocarray(void *optr, size_t nmemb, size_t size) { + if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) && + nmemb > 0 && SIZE_MAX / nmemb < size) { + errno = ENOMEM; + return NULL; + } + /* + * Head off variations in realloc behavior on different + * platforms (reported by MarkR ) + * + * The behaviour of reallocarray is implementation-defined if + * nmemb or size is zero. It can return NULL or non-NULL + * depending on the platform. + * https://www.securecoding.cert.org/confluence/display/c/MEM04-C.Beware+of+zero-lengthallocations + * + * Here are some extracts from realloc man pages on different platforms. + * + * void realloc( void memblock, size_t size ); + * + * Windows: + * + * If there is not enough available memory to expand the block + * to the given size, the original block is left unchanged, + * and NULL is returned. If size is zero, then the block + * pointed to by memblock is freed; the return value is NULL, + * and memblock is left pointing at a freed block. + * + * OpenBSD: + * + * If size or nmemb is equal to 0, a unique pointer to an + * access protected, zero sized object is returned. Access via + * this pointer will generate a SIGSEGV exception. + * + * Linux: + * + * If size was equal to 0, either NULL or a pointer suitable + * to be passed to free() is returned. + * + * OS X: + * + * If size is zero and ptr is not NULL, a new, minimum sized + * object is allocated and the original object is freed. + * + * It looks like images with zero width or height can trigger + * this, and fuzzing behaviour will differ by platform, so + * fuzzing on one platform may not detect zero-size allocation + * problems on other platforms. + */ + if (size == 0 || nmemb == 0) { + return NULL; + } + return realloc(optr, size * nmemb); +} diff --git a/pyproject.toml b/pyproject.toml index e3ce749..fc77316 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "tensorcodec" version = "0.1.5" -description = "NumPy audio/video decoding with TorchCodec-compatible playback semantics" +description = "NumPy audio/video/image decoding with TorchCodec-style APIs" readme = "README.md" license = "Apache-2.0" license-files = ["LICENSE", "licenses/*"] @@ -22,7 +22,7 @@ Repository = "https://github.com/MilkClouds/tensorcodec" Issues = "https://github.com/MilkClouds/tensorcodec/issues" [dependency-groups] -dev = ["pytest>=8", "ruff>=0.9", "maturin>=1.8,<2"] +dev = ["pytest>=8", "ruff>=0.9", "maturin>=1.8,<2", "Pillow>=11.3"] oracle = ["torch==2.14.1", "torchcodec==0.17.0"] [build-system] @@ -40,8 +40,10 @@ include = [ { path = "docs/*.md", format = "sdist" }, { path = "packaging/*.json", format = "sdist" }, { path = "tests/**/*.py", format = "sdist" }, + { path = "tests/resources/images/*", format = "sdist" }, { path = "benchmarks/**/*.py", format = "sdist" }, { path = "benchmarks/*.md", format = "sdist" }, + { path = "benchmarks/image_candidates/**", format = "sdist" }, ] [tool.pytest.ini_options] diff --git a/scripts/build_image_deps.sh b/scripts/build_image_deps.sh new file mode 100644 index 0000000..c9c8e75 --- /dev/null +++ b/scripts/build_image_deps.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Dedicated image decoders, with no encoder tools. PNG and GIF are built by Cargo. +set -euo pipefail +prefix="${1:?usage: build_image_deps.sh ABSOLUTE_INSTALL_PREFIX}" +root="$(mktemp -d)" +jobs="${TENSORCODEC_BUILD_JOBS:-4}" +fetch() { + curl -fLsS "$2" -o "$root/$1.tar.gz" + echo "$3 $root/$1.tar.gz" | sha256sum --check + mkdir "$root/$1" + tar -xf "$root/$1.tar.gz" -C "$root/$1" --strip-components=1 +} +build() { + local name="$1" + shift + cmake -S "$root/$name" -B "$root/$name-build" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$prefix" \ + -DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_POSITION_INDEPENDENT_CODE=ON "$@" + cmake --build "$root/$name-build" --parallel "$jobs" + cmake --install "$root/$name-build" +} +fetch jpeg https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.tar.gz \ + 980dd81f425082aa6d7c9e47fef27554ce7a9ffc8e2f6e863b97d263c5c50858 +build jpeg -DENABLE_SHARED=ON -DENABLE_STATIC=OFF -DWITH_TURBOJPEG=OFF -DWITH_TOOLS=OFF -DWITH_TESTS=OFF +fetch webp https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.6.0.tar.gz \ + e4ab7009bf0629fd11982d4c2aa83964cf244cffba7347ecd39019a9e38c4564 +build webp -DBUILD_SHARED_LIBS=ON -DWEBP_BUILD_ANIM_UTILS=OFF -DWEBP_BUILD_CWEBP=OFF \ + -DWEBP_BUILD_DWEBP=OFF -DWEBP_BUILD_GIF2WEBP=OFF -DWEBP_BUILD_IMG2WEBP=OFF \ + -DWEBP_BUILD_VWEBP=OFF -DWEBP_BUILD_WEBPINFO=OFF -DWEBP_BUILD_WEBPMUX=OFF \ + -DWEBP_BUILD_EXTRAS=OFF +fetch avif https://github.com/AOMediaCodec/libavif/archive/refs/tags/v1.4.2.tar.gz \ + 2b645287340ba5a631d268b551dc2d72bd73ac33335962dd36dcdb6d8366921d +# libavif's pinned SIMD color converter, also used by TorchCodec 0.17.0. +git init -q "$root/avif/ext/libyuv" +git -C "$root/avif/ext/libyuv" remote add origin https://chromium.googlesource.com/libyuv/libyuv +git -C "$root/avif/ext/libyuv" fetch --depth=1 origin 644251f252a84bf8ce91ff0aca86a9b16b069ab8 +git -C "$root/avif/ext/libyuv" checkout --detach FETCH_HEAD +test "$(git -C "$root/avif/ext/libyuv" rev-parse HEAD)" = 644251f252a84bf8ce91ff0aca86a9b16b069ab8 +bash "$(dirname "$0")/build_dav1d.sh" "$root/dav1d" +export PKG_CONFIG_PATH="$root/dav1d/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" +build avif -DBUILD_SHARED_LIBS=ON -DAVIF_CODEC_DAV1D=SYSTEM -DAVIF_LIBYUV=LOCAL \ + -DAVIF_CODEC_AOM=OFF -DAVIF_CODEC_RAV1E=OFF -DAVIF_CODEC_SVT=OFF -DAVIF_CODEC_LIBGAV1=OFF \ + -DAVIF_CODEC_AVM=OFF -DAVIF_LIBSHARPYUV=OFF -DAVIF_JPEG=OFF -DAVIF_ZLIBPNG=OFF \ + -DAVIF_LIBXML2=OFF -DAVIF_BUILD_APPS=OFF -DAVIF_BUILD_TESTS=OFF -DAVIF_BUILD_EXAMPLES=OFF +# This prefix ships only shared libavif, with dav1d/libyuv statically embedded. +# Its consumers need neither dependency's development package or static archive. +sed '/^Requires.private:/d; /^Libs.private:/d' "$prefix/lib/pkgconfig/libavif.pc" > "$root/libavif.pc" +mv "$root/libavif.pc" "$prefix/lib/pkgconfig/libavif.pc" diff --git a/scripts/build_macos_wheel.sh b/scripts/build_macos_wheel.sh index db681a3..db88a63 100644 --- a/scripts/build_macos_wheel.sh +++ b/scripts/build_macos_wheel.sh @@ -8,9 +8,11 @@ export PKG_CONFIG_PATH="$build_prefix/openssl/lib/pkgconfig${PKG_CONFIG_PATH:+:$ if [ ! -f "$build_prefix/ready" ]; then bash scripts/build_openssl.sh "$build_prefix/openssl" bash scripts/build_ffmpeg.sh "$build_prefix/ffmpeg" + bash scripts/build_image_deps.sh "$build_prefix/images" touch "$build_prefix/ready" fi export FFMPEG_DIR="$build_prefix/ffmpeg" +export PKG_CONFIG_PATH="$build_prefix/images/lib/pkgconfig:$PKG_CONFIG_PATH" maturin build --release --locked --out unrepaired delocate-wheel --require-archs "$(uname -m)" -w dist unrepaired/*.whl python scripts/check_wheel_size.py dist/*.whl diff --git a/scripts/check_wheel_runtime.py b/scripts/check_wheel_runtime.py index d2cfa15..2310106 100644 --- a/scripts/check_wheel_runtime.py +++ b/scripts/check_wheel_runtime.py @@ -86,13 +86,34 @@ def generate(root: Path): check=True, ) + from PIL import Image + + from tensorcodec.decoders import decode_image + + image = Image.fromarray(np.random.default_rng(42).integers(0, 256, (29, 37, 3), dtype=np.uint8)) + for codec in ("JPEG", "PNG", "WEBP", "GIF", "AVIF"): + path = root / f"image.{codec.lower()}" + image.save(path, format=codec, max_threads=1) + np.save(root / f"image.{codec.lower()}.npy", decode_image(path)) + for codec in ("WEBP", "GIF", "AVIF"): + path = root / f"animated.{codec.lower()}" + image.save( + path, + format=codec, + save_all=True, + append_images=[image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)], + duration=100, + max_threads=1, + ) + np.save(root / f"animated.{codec.lower()}.npy", decode_image(path)) + def check(root: Path): import numpy as np from tensorcodec.decoders import AudioDecoder, VideoDecoder - assert all(importlib.util.find_spec(name) is None for name in ("torch", "torchcodec", "av")) + assert all(importlib.util.find_spec(name) is None for name in ("torch", "torchcodec", "av", "PIL")) with VideoDecoder(root / "video.mp4") as decoder: assert len(decoder) == 10 frames = decoder.get_frames_at([7, 0, 7]) @@ -123,6 +144,12 @@ def check(root: Path): batch = decoder.get_frames_at([2, 0, 2]) assert batch.pixel_format == fmt and batch.data.dtype.isnative np.testing.assert_array_equal(batch.data, expected[[2, 0, 2]].transpose(0, 3, 1, 2)) + from tensorcodec.decoders import decode_image + + for path in root.glob("image.*.npy"): + np.testing.assert_array_equal(decode_image(path.with_suffix("")), np.load(path)) + for path in root.glob("animated.*.npy"): + np.testing.assert_array_equal(decode_image(path.with_suffix("")), np.load(path)) print(f"Wheel decoding passed: Python {platform.python_version()}, {platform.machine()}, {platform.libc_ver()}") diff --git a/src/tensorcodec/decoders/__init__.py b/src/tensorcodec/decoders/__init__.py index 7c13c8e..8525658 100644 --- a/src/tensorcodec/decoders/__init__.py +++ b/src/tensorcodec/decoders/__init__.py @@ -1,4 +1,28 @@ from tensorcodec._metadata import AudioStreamMetadata, VideoStreamMetadata from tensorcodec.decoders._decoder import AudioDecoder, CpuFallbackStatus, VideoDecoder +from tensorcodec.decoders._images import ( + ImageReadMode, + decode_avif, + decode_gif, + decode_heic, + decode_image, + decode_jpeg, + decode_png, + decode_webp, +) -__all__ = ["AudioDecoder", "AudioStreamMetadata", "CpuFallbackStatus", "VideoDecoder", "VideoStreamMetadata"] +__all__ = [ + "AudioDecoder", + "AudioStreamMetadata", + "CpuFallbackStatus", + "ImageReadMode", + "VideoDecoder", + "VideoStreamMetadata", + "decode_avif", + "decode_gif", + "decode_heic", + "decode_image", + "decode_jpeg", + "decode_png", + "decode_webp", +] diff --git a/src/tensorcodec/decoders/_image_libraries.py b/src/tensorcodec/decoders/_image_libraries.py new file mode 100644 index 0000000..58c0f7a --- /dev/null +++ b/src/tensorcodec/decoders/_image_libraries.py @@ -0,0 +1,108 @@ +"""Optional system libheif binding for HEIC decoding.""" + +import ctypes as c +from ctypes.util import find_library +from functools import lru_cache + +import numpy as np + + +@lru_cache +def _library(name): + path = find_library(name) + if path is None: + raise ImportError(f"image decoding requires the system library lib{name}") + try: + return c.CDLL(path) + except OSError as exc: + raise ImportError(f"cannot load lib{name}: {exc}") from exc + + +def _function(library, name, result, *arguments): + function = getattr(library, name) + function.restype, function.argtypes = result, arguments + return function + + +class _HeifError(c.Structure): + _fields_ = [("code", c.c_int), ("subcode", c.c_int), ("message", c.c_char_p)] + + +def _check(error): + if error.code: + raise RuntimeError(error.message.decode("utf-8", errors="replace") if error.message else "libheif error") + + +def heic(data, high_depth): + library = _library("heif") + pointer = c.c_void_p + allocate = _function(library, "heif_context_alloc", pointer) + free = _function(library, "heif_context_free", None, pointer) + read = _function( + library, "heif_context_read_from_memory_without_copy", _HeifError, pointer, pointer, c.c_size_t, pointer + ) + count = _function(library, "heif_context_get_number_of_top_level_images", c.c_int, pointer) + ids = _function( + library, "heif_context_get_list_of_top_level_image_IDs", c.c_int, pointer, c.POINTER(c.c_uint32), c.c_int + ) + get_handle = _function( + library, "heif_context_get_image_handle", _HeifError, pointer, c.c_uint32, c.POINTER(pointer) + ) + release_handle = _function(library, "heif_image_handle_release", None, pointer) + depth = _function(library, "heif_image_handle_get_luma_bits_per_pixel", c.c_int, pointer) + alpha = _function(library, "heif_image_handle_has_alpha_channel", c.c_int, pointer) + decode = _function( + library, "heif_decode_image", _HeifError, pointer, c.POINTER(pointer), c.c_int, c.c_int, pointer + ) + release_image = _function(library, "heif_image_release", None, pointer) + width = _function(library, "heif_image_get_width", c.c_int, pointer, c.c_int) + height = _function(library, "heif_image_get_height", c.c_int, pointer, c.c_int) + plane = _function( + library, "heif_image_get_plane_readonly", c.POINTER(c.c_uint8), pointer, c.c_int, c.POINTER(c.c_int) + ) + context = allocate() + if not context: + raise MemoryError("cannot allocate HEIC context") + buffer = c.create_string_buffer(data) + try: + _check(read(context, buffer, len(data), None)) + size = count(context) + if size <= 0: + raise RuntimeError("HEIC contains no images") + items = (c.c_uint32 * size)() + if ids(context, items, size) != size: + raise RuntimeError("incomplete HEIC image list") + frames, layout = [], None + for item in items: + handle, image = pointer(), pointer() + try: + _check(get_handle(context, item, c.byref(handle))) + bits, channels = depth(handle), 4 if alpha(handle) else 3 + if bits not in (8, 10, 12, 16): + raise RuntimeError(f"unsupported HEIC bit depth: {bits}") + high = bits > 8 and high_depth + chroma = (14 if high else 10) + (channels == 4) # Interleaved RGB(A), little-endian for 16-bit. + _check(decode(handle, c.byref(image), 1, chroma, None)) + w, h, stride = width(image, 10), height(image, 10), c.c_int() + pixels = plane(image, 10, c.byref(stride)) + row_size = w * channels * (2 if high else 1) + if not pixels or w <= 0 or h <= 0 or stride.value < row_size: + raise RuntimeError("invalid HEIC image plane") + current = (h, w, channels, bits) + if layout is not None and current != layout: + raise RuntimeError("HEIC images have different dimensions, channels or bit depths") + layout = current + rows = np.ctypeslib.as_array(pixels, shape=(h * stride.value,)).reshape(h, stride.value) + frame = rows[:, :row_size].copy().view("> (bits - shift)) + frames.append(frame) + finally: + if image: + release_image(image) + if handle: + release_handle(handle) + return np.stack(frames) + finally: + free(context) diff --git a/src/tensorcodec/decoders/_image_orientation.py b/src/tensorcodec/decoders/_image_orientation.py new file mode 100644 index 0000000..ac0cbd1 --- /dev/null +++ b/src/tensorcodec/decoders/_image_orientation.py @@ -0,0 +1,85 @@ +"""JPEG, PNG and WebP EXIF orientation.""" + +import numpy as np + + +def _tiff_orientation(data): + if len(data) < 8 or data[:2] not in (b"II", b"MM"): + return 1 + order = "little" if data[:2] == b"II" else "big" + if int.from_bytes(data[2:4], order) != 42: + return 1 + offset = int.from_bytes(data[4:8], order) + if offset + 2 > len(data): + return 1 + count = int.from_bytes(data[offset : offset + 2], order) + for index in range(count): + entry = offset + 2 + index * 12 + if entry + 12 > len(data): + return 1 + if int.from_bytes(data[entry : entry + 2], order) == 274: + if ( + int.from_bytes(data[entry + 2 : entry + 4], order) != 3 + or int.from_bytes(data[entry + 4 : entry + 8], order) != 1 + ): + return 1 + value = int.from_bytes(data[entry + 8 : entry + 10], order) + return value if 1 <= value <= 8 else 1 + return 1 + + +def orientation(data, codec): + if codec == "webp": + offset = 12 + while offset + 8 <= len(data): + size = int.from_bytes(data[offset + 4 : offset + 8], "little") + if offset + 8 + size > len(data): + break + if data[offset : offset + 4] == b"EXIF": + payload = data[offset + 8 : offset + 8 + size] + return _tiff_orientation(payload.removeprefix(b"Exif\0\0")) + offset += 8 + size + (size & 1) + elif codec == "png": + offset = 8 + while offset + 12 <= len(data): + size = int.from_bytes(data[offset : offset + 4], "big") + if offset + 12 + size > len(data): + break + if data[offset + 4 : offset + 8] == b"eXIf": + return _tiff_orientation(data[offset + 8 : offset + 8 + size]) + offset += size + 12 + elif codec == "jpeg": + offset = 2 + while offset < len(data) and data[offset] == 0xFF: + while offset < len(data) and data[offset] == 0xFF: + offset += 1 + if offset >= len(data) or data[offset] in (0xDA, 0xD9): + break + marker, offset = data[offset], offset + 1 + size = int.from_bytes(data[offset : offset + 2], "big") + if size < 2 or offset + size > len(data): + break + payload = data[offset + 2 : offset + size] + if marker == 0xE1 and payload.startswith(b"Exif\0\0"): + return _tiff_orientation(payload[6:]) + offset += size + return 1 + + +def apply_orientation(images, value): + # NHWC throughout; the public wrapper moves channels only after orientation. + if value == 2: + return images[:, :, ::-1] + if value == 3: + return images[:, ::-1, ::-1] + if value == 4: + return images[:, ::-1] + if value == 5: + return images.swapaxes(1, 2) + if value == 6: + return np.rot90(images, -1, axes=(1, 2)) + if value == 7: + return images.swapaxes(1, 2)[:, ::-1, ::-1] + if value == 8: + return np.rot90(images, 1, axes=(1, 2)) + return images diff --git a/src/tensorcodec/decoders/_images.py b/src/tensorcodec/decoders/_images.py new file mode 100644 index 0000000..fe3361c --- /dev/null +++ b/src/tensorcodec/decoders/_images.py @@ -0,0 +1,216 @@ +"""CPU image decoding with TorchCodec's function API and NumPy output.""" + +from enum import Enum +from pathlib import Path + +import numpy as np + +from tensorcodec._native import decode_image as _decode +from tensorcodec.decoders._image_orientation import apply_orientation, orientation + + +class ImageReadMode(Enum): + UNCHANGED = 0 + GRAY = 1 + GRAY_ALPHA = 2 + RGB = 3 + RGB_ALPHA = 4 + RGBA = RGB_ALPHA + + +def _mode(value): + if isinstance(value, ImageReadMode): + return value + if isinstance(value, str): + try: + return ImageReadMode[value.upper()] + except KeyError: + raise ValueError(f"Invalid image mode: {value!r}") from None + raise TypeError("mode must be a string or ImageReadMode") + + +def _dtype(value): + if isinstance(value, str) and value == "auto": + return value + try: + dtype = np.dtype(value) + except (TypeError, ValueError): + raise ValueError("output_dtype must be uint8, uint16 or 'auto'") from None + if dtype not in (np.dtype("uint8"), np.dtype("uint16")): + raise ValueError("output_dtype must be uint8, uint16 or 'auto'") + return dtype + + +def _bytes(source): + if isinstance(source, (str, Path)): + return Path(source).read_bytes() + if isinstance(source, (bytes, bytearray)): + return bytes(source) + if isinstance(source, np.ndarray): + if source.dtype != np.uint8 or source.ndim != 1: + raise ValueError("encoded image arrays must be one-dimensional uint8") + return source.tobytes() + raise TypeError("source must be a path, bytes or a one-dimensional uint8 NumPy array") + + +def _format(data): + if data.startswith(b"\xff\xd8\xff"): + return "jpeg" + if data.startswith(b"\x89PNG\r\n\x1a\n"): + return "png" + if data[:6] in (b"GIF87a", b"GIF89a"): + return "gif" + if data[:4] == b"RIFF" and data[8:12] == b"WEBP": + return "webp" + if data[4:8] == b"ftyp": + size = int.from_bytes(data[:4], "big") + brands = [data[8:12], *[data[i : i + 4] for i in range(16, min(size, len(data)), 4)]] + if any(b in (b"avif", b"avis") for b in brands): + return "avif" + if any(b in (b"heic", b"heix", b"heim", b"heis", b"hevc", b"hevx", b"mif1", b"msf1") for b in brands): + return "heic" + raise ValueError("Unsupported or unrecognized image format") + + +def _png_channels(data): + channels = None + offset = 8 + while offset + 12 <= len(data): + size = int.from_bytes(data[offset : offset + 4], "big") + kind = data[offset + 4 : offset + 8] + if offset + size + 12 > len(data): + raise RuntimeError("truncated PNG chunk") + if kind == b"IHDR" and size == 13: + channels = {0: 1, 2: 3, 3: 3, 4: 2, 6: 4}.get(data[offset + 17]) + elif kind == b"tRNS": + channels = 2 if channels == 1 else 4 + elif kind == b"acTL": + raise NotImplementedError("animated PNG decoding is unsupported") + elif kind == b"IDAT": + break + offset += size + 12 + if channels is None: + raise RuntimeError("PNG has no valid IHDR") + return channels + + +def _color(images, mode, codec): + channels = images.shape[-1] + if mode is ImageReadMode.UNCHANGED: + return images + if channels == mode.value: + return images + gray_source = channels < 3 + color = images[..., :1] if gray_source else images[..., :3] + alpha = images[..., -1:] if channels in (2, 4) else np.full_like(images[..., :1], np.iinfo(images.dtype).max) + if mode in (ImageReadMode.GRAY, ImageReadMode.GRAY_ALPHA): + if not gray_source: + if codec == "png": + # TorchCodec requests libpng coefficients 0.2989 and 0.587. + color = ( + (color.astype(np.uint64) * np.array([9794, 19234, 3740], np.uint64)).sum(axis=-1, keepdims=True) + >> 15 + ).astype(images.dtype) + else: + color = ( + np.rint( + (color.astype(np.float32) * np.array([0.2989, 0.587, 0.114], np.float32)).sum( + axis=-1, keepdims=True + ) + ) + .clip(0, np.iinfo(images.dtype).max) + .astype(images.dtype) + ) + elif gray_source: + color = np.repeat(color, 3, axis=-1) + return ( + np.concatenate((color, alpha), axis=-1) + if mode in (ImageReadMode.GRAY_ALPHA, ImageReadMode.RGB_ALPHA) + else color + ) + + +def _image(source, codec, mode, output_dtype, threads=1): + mode, dtype = _mode(mode), _dtype(output_dtype) + data = _bytes(source) + found = _format(data) + if codec is not None and found != codec: + raise RuntimeError(f"expected {codec}, got {found}") + codec = found + orient = orientation(data, codec) + animated = False + if codec == "png": + _png_channels(data) # Reject APNG rather than silently returning its first frame. + if codec == "heic": + from tensorcodec.decoders._image_libraries import heic + + images = heic(data, high_depth=dtype != np.uint8) + else: + depth = 0 if dtype == "auto" else np.dtype(dtype).itemsize * 8 + images, native_orientation, animated = _decode( + data, ("jpeg", "png", "webp", "gif", "avif").index(codec), mode.value, depth, threads + ) + if codec == "avif": + orient = native_orientation + images = _color(images, mode, codec) + if dtype != "auto" and images.dtype != dtype: + if dtype == np.uint16: + images = images.astype(np.uint16) * 257 + else: + images = np.rint(images.astype(np.float32) / 257).clip(0, 255).astype(np.uint8) + images = apply_orientation(images, orient).transpose(0, 3, 1, 2) + keep_batch = codec == "webp" and animated + return images[0] if len(images) == 1 and not keep_batch else images + + +def decode_image(source, *, mode="RGB", output_dtype=np.uint8): + """Detect encoded content and return a CHW image or NCHW animation on CPU. + + Sources are paths, bytes or 1-D uint8 arrays. Modes: UNCHANGED, GRAY, + GRAY_ALPHA, RGB, RGB_ALPHA (case-insensitive strings or ImageReadMode). + output_dtype is uint8, uint16 or 'auto'; integer conversion scales the range. + HEIC requires system libheif. Other image codecs are bundled in wheels. + Animated PNG is unsupported. + """ + return _image(source, None, mode, output_dtype) + + +def decode_jpeg(source, *, mode="RGB", output_dtype=np.uint8, device="cpu"): + """Decode a JPEG to CHW, or a list/tuple of sources to a list of CHW arrays. + + Only CPU decoding is supported. Batches may have different image dimensions. + """ + if str(device) != "cpu": + raise ValueError("only CPU image decoding is supported") + if isinstance(source, (list, tuple)): + _mode(mode) + _dtype(output_dtype) + return [_image(item, "jpeg", mode, output_dtype) for item in source] + return _image(source, "jpeg", mode, output_dtype) + + +def decode_png(source, *, mode="RGB", output_dtype=np.uint8): + """Decode a PNG to CHW; 'auto' preserves native 8/16-bit sample precision.""" + return _image(source, "png", mode, output_dtype) + + +def decode_webp(source, *, mode="RGB", output_dtype=np.uint8): + """Decode WebP to CHW/NCHW. Animations are composited with libwebpdemux.""" + return _image(source, "webp", mode, output_dtype) + + +def decode_gif(source, *, mode="RGB", output_dtype=np.uint8): + """Decode GIF to CHW for one frame or NCHW for multiple frames.""" + return _image(source, "gif", mode, output_dtype) + + +def decode_avif(source, *, mode="RGB", output_dtype=np.uint8, num_threads=1): + """Decode AVIF to CHW/NCHW; num_threads controls libavif workers.""" + if not isinstance(num_threads, int) or isinstance(num_threads, bool) or num_threads < 1: + raise ValueError("num_threads must be a positive integer") + return _image(source, "avif", mode, output_dtype, num_threads) + + +def decode_heic(source, *, mode="RGB", output_dtype=np.uint8): + """Decode HEIC/HEIF to CHW/NCHW using optional system libheif.""" + return _image(source, "heic", mode, output_dtype) diff --git a/tests/resources/images/LICENSE.torchcodec b/tests/resources/images/LICENSE.torchcodec new file mode 100644 index 0000000..bc559a9 --- /dev/null +++ b/tests/resources/images/LICENSE.torchcodec @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright 2024 Meta + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice,this list +of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may +be used to endorse or promote products derived from this software without specific +prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/tests/resources/images/README.md b/tests/resources/images/README.md new file mode 100644 index 0000000..5014b43 --- /dev/null +++ b/tests/resources/images/README.md @@ -0,0 +1,8 @@ +# HEIC fixtures + +Copied unchanged from `pytorch/torchcodec` tag `v0.17.0`, `test/resources/`: +`gradient_10bit.heic`, `rgba.heic`, and `animated.heic`. +Source: https://github.com/pytorch/torchcodec/tree/v0.17.0/test/resources +The upstream BSD license is included as `LICENSE.torchcodec`. + +These fixtures cover independent HEIC content, high bit depth, alpha and multiple images. diff --git a/tests/resources/images/animated.heic b/tests/resources/images/animated.heic new file mode 100644 index 0000000..4345bed Binary files /dev/null and b/tests/resources/images/animated.heic differ diff --git a/tests/resources/images/gradient_10bit.heic b/tests/resources/images/gradient_10bit.heic new file mode 100644 index 0000000..20cb8bc Binary files /dev/null and b/tests/resources/images/gradient_10bit.heic differ diff --git a/tests/resources/images/rgba.heic b/tests/resources/images/rgba.heic new file mode 100644 index 0000000..6a2173d Binary files /dev/null and b/tests/resources/images/rgba.heic differ diff --git a/tests/test_images.py b/tests/test_images.py new file mode 100644 index 0000000..0407406 --- /dev/null +++ b/tests/test_images.py @@ -0,0 +1,520 @@ +"""Image API contracts from known PNG samples and independent FFmpeg encodings.""" + +import shutil +import struct +import zlib +from io import BytesIO +from pathlib import Path + +import numpy as np +import pytest +from PIL import Image + +from tests.utils import as_numpy, run_ffmpeg + + +def chunk(kind, data): + return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data)) + + +def png_bytes(pixels): + height, width, channels = pixels.shape + depth = pixels.dtype.itemsize * 8 + color = {1: 0, 2: 4, 3: 2, 4: 6}[channels] + + raw = pixels.astype(">u2" if depth == 16 else np.uint8).tobytes() + stride = width * channels * (depth // 8) + scanlines = b"".join(b"\0" + raw[i : i + stride] for i in range(0, len(raw), stride)) + return ( + b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, depth, color, 0, 0, 0)) + + chunk(b"IDAT", zlib.compress(scanlines)) + + chunk(b"IEND", b"") + ) + + +def dtype_for(backend, name): + if name == "auto": + return name + if backend.__name__.startswith("torchcodec"): + import torch + + return getattr(torch, name) + return getattr(np, name) + + +@pytest.fixture(scope="module") +def encoded_images(tmp_path_factory): + root = tmp_path_factory.mktemp("images") + pixels = np.arange(16 * 24 * 3, dtype=np.uint8).reshape(16, 24, 3) + (root / "source.png").write_bytes(png_bytes(pixels)) + for codec, options in { + "jpeg": ["-c:v", "mjpeg", "-q:v", "1", "-pix_fmt", "yuvj444p"], + "gif": [], + "avif": ["-c:v", "libaom-av1", "-still-picture", "1", "-crf", "0", "-cpu-used", "8"], + }.items(): + run_ffmpeg("-i", root / "source.png", "-frames:v", 1, *options, "-threads", 1, root / f"image.{codec}") + Image.fromarray(pixels).save(root / "image.webp", lossless=True) + (root / "second.png").write_bytes(png_bytes(255 - pixels)) + run_ffmpeg( + "-framerate", + 2, + "-pattern_type", + "glob", + "-i", + str(root / "s*.png"), + "-threads", + 1, + root / "animated.gif", + ) + return root + + +@pytest.mark.parametrize("channels", [1, 2, 3, 4]) +@pytest.mark.parametrize("depth", [8, 16]) +def test_png_native_samples(backend, channels, depth): + dtype = np.uint8 if depth == 8 else np.uint16 + pixels = np.arange(5 * 7 * channels, dtype=dtype).reshape(5, 7, channels) + if depth == 16: + pixels = pixels * 311 + decoded = backend.decode_png(png_bytes(pixels), mode="UNCHANGED", output_dtype="auto") + np.testing.assert_array_equal(as_numpy(decoded), pixels.transpose(2, 0, 1)) + assert as_numpy(decoded).dtype == dtype + + +@pytest.mark.parametrize("mode,channels", [("RGB", 3), ("GRAY", 1), ("RGB_ALPHA", 4), ("GRAY_ALPHA", 2)]) +def test_png_color_modes(backend, mode, channels): + pixels = np.array([[[255, 0, 0, 17], [0, 255, 0, 93], [0, 0, 255, 201]]], np.uint8) + result = as_numpy(backend.decode_image(png_bytes(pixels), mode=mode.lower())) + assert result.shape == (channels, 1, 3) + if "ALPHA" in mode: + np.testing.assert_array_equal(result[-1], pixels[..., 3]) + if mode.startswith("RGB"): + np.testing.assert_array_equal(result[:3], pixels[..., :3].transpose(2, 0, 1)) + else: + np.testing.assert_array_equal(result[0], [[76, 149, 29]]) + + +def test_output_dtype_scales_values(backend): + pixels = np.array([[[0], [1], [128], [255]]], np.uint8) + result = backend.decode_png(png_bytes(pixels), mode="GRAY", output_dtype=dtype_for(backend, "uint16")) + np.testing.assert_array_equal(as_numpy(result), pixels.transpose(2, 0, 1).astype(np.uint16) * 257) + pixels = np.array([[[0], [128], [129], [32768], [65535]]], np.uint16) + result = backend.decode_png(png_bytes(pixels), mode="GRAY") + np.testing.assert_array_equal(as_numpy(result), np.rint(pixels.transpose(2, 0, 1) / 257).astype(np.uint8)) + + +@pytest.mark.parametrize("kind", ["bytes", "bytearray", "path", "str", "array"]) +def test_image_sources_and_content_detection(backend, tmp_path, kind): + pixels = np.arange(27, dtype=np.uint8).reshape(3, 3, 3) + data = png_bytes(pixels) + path = tmp_path / "misleading.jpg" + path.write_bytes(data) + sources = {"bytes": data, "bytearray": bytearray(data), "path": path, "str": str(path)} + if kind == "array": + if backend.__name__.startswith("torchcodec"): + import torch + + source = torch.frombuffer(bytearray(data), dtype=torch.uint8) + else: + source = np.frombuffer(data, dtype=np.uint8) + else: + source = sources[kind] + np.testing.assert_array_equal(as_numpy(backend.decode_image(source)), pixels.transpose(2, 0, 1)) + + +@pytest.mark.parametrize("codec", ["jpeg", "webp", "gif", "avif"]) +def test_format_functions_and_dispatch(backend, encoded_images, codec): + path = encoded_images / f"image.{codec}" + direct = as_numpy(getattr(backend, f"decode_{codec}")(path)) + assert direct.shape == (3, 16, 24) + assert direct.dtype == np.uint8 + np.testing.assert_array_equal(direct, as_numpy(backend.decode_image(path.read_bytes()))) + + +def test_jpeg_batch(backend, encoded_images): + source = encoded_images / "image.jpeg" + images = backend.decode_jpeg([source, source.read_bytes()]) + assert isinstance(images, list) + assert len(images) == 2 + np.testing.assert_array_equal(as_numpy(images[0]), as_numpy(images[1])) + images[0][...] = 0 + assert as_numpy(images[1]).any() + assert backend.decode_jpeg([]) == [] + + +def test_gif_animation(backend, encoded_images): + frames = as_numpy(backend.decode_gif(encoded_images / "animated.gif")) + assert frames.shape == (2, 3, 16, 24) + assert not np.array_equal(frames[0], frames[1]) + + +@pytest.mark.parametrize("mode", ["RGB", "UNCHANGED", "GRAY", "GRAY_ALPHA", "RGB_ALPHA"]) +@pytest.mark.parametrize("codec", ["jpeg", "png", "webp", "gif", "avif"]) +def test_image_differential(oracle, encoded_images, codec, mode): + import tensorcodec.decoders as actual + + path = encoded_images / ("source.png" if codec == "png" else f"image.{codec}") + got = getattr(actual, f"decode_{codec}")(path, mode=mode) + expected = as_numpy(getattr(oracle, f"decode_{codec}")(path, mode=mode)) + assert got.shape == expected.shape + np.testing.assert_allclose(got.astype(np.int32), expected.astype(np.int32), atol=2, rtol=0) + + +def test_image_errors_and_cpu_boundary(): + from tensorcodec.decoders import decode_avif, decode_image, decode_jpeg, decode_png + + data = png_bytes(np.zeros((2, 3, 3), np.uint8)) + with pytest.raises(ValueError, match="mode"): + decode_png(data, mode="BGR") + with pytest.raises(ValueError, match="output_dtype"): + decode_png(data, output_dtype=np.float32) + with pytest.raises(ValueError, match="one-dimensional"): + decode_png(np.zeros((2, 3), np.uint8)) + with pytest.raises(TypeError, match="source"): + decode_image(object()) + with pytest.raises(ValueError, match="CPU"): + decode_jpeg(b"", device="cuda") + with pytest.raises(RuntimeError, match="expected jpeg"): + decode_jpeg(data) + with pytest.raises(ValueError, match="unrecognized"): + decode_image(b"not an image") + with pytest.raises(ValueError, match="num_threads"): + decode_avif(b"", num_threads=0) + with pytest.raises(FileNotFoundError): + decode_image(Path("/nonexistent/image.png")) + + +def test_optional_libraries_report_missing_dependencies(monkeypatch): + from tensorcodec.decoders import _image_libraries, decode_image + + monkeypatch.setattr(_image_libraries, "find_library", lambda name: None) + _image_libraries._library.cache_clear() + heic = struct.pack(">I", 24) + b"ftypheic" + b"\0" * 4 + b"mif1heic" + with pytest.raises(ImportError, match="libheif"): + decode_image(heic) + + +@pytest.fixture(scope="module") +def optional_images(tmp_path_factory): + root = tmp_path_factory.mktemp("optional-images") + pixels = np.zeros((16, 24, 4), np.uint8) + pixels[..., :3] = [30, 60, 90] + pixels[..., 3] = 255 + pixels[:8, :12, 3] = 0 + image = Image.fromarray(pixels) + second = image.copy() + second.paste((120, 50, 10, 128), (8, 4, 16, 12)) + image.save(root / "animation.webp", save_all=True, append_images=[second], lossless=True, duration=100) + fixtures = Path(__file__).parent / "resources/images" + for source, target in [ + ("rgba.heic", "alpha.heic"), + ("animated.heic", "multi.heic"), + ("gradient_10bit.heic", "high.heic"), + ]: + shutil.copyfile(fixtures / source, root / target) + return root + + +@pytest.mark.parametrize("file", ["animation.webp", "alpha.heic", "multi.heic", "high.heic"]) +@pytest.mark.parametrize("mode", ["RGB", "UNCHANGED", "GRAY", "GRAY_ALPHA", "RGB_ALPHA"]) +def test_optional_image_differential(oracle, optional_images, file, mode): + from tensorcodec.decoders import _image_libraries, decode_image + + library = "webpdemux" if file.endswith("webp") else "heif" + try: + _image_libraries._library(library) + except ImportError: + pytest.skip(f"optional system library {library} unavailable") + path = optional_images / file + got = decode_image(path, mode=mode) + expected = as_numpy(oracle.decode_image(path, mode=mode)) + assert got.shape == expected.shape + np.testing.assert_allclose(got.astype(np.int32), expected.astype(np.int32), atol=2, rtol=0) + + +@pytest.mark.parametrize("dtype", ["auto", "uint16"]) +def test_heic_high_depth(oracle, optional_images, dtype): + from tensorcodec.decoders import _image_libraries, decode_heic + + try: + _image_libraries._library("heif") + except ImportError: + pytest.skip("optional system libheif unavailable") + actual = decode_heic(optional_images / "high.heic", output_dtype=dtype) + expected = as_numpy( + oracle.decode_heic( + optional_images / "high.heic", output_dtype="auto" if dtype == "auto" else dtype_for(oracle, dtype) + ) + ) + assert actual.dtype == np.uint16 + np.testing.assert_array_equal(actual, expected) + + +def test_single_frame_animated_webp_keeps_batch_dimension(oracle, optional_images): + from tensorcodec.decoders import _image_libraries, decode_webp + + try: + _image_libraries._library("webpdemux") + except ImportError: + pytest.skip("optional libwebpdemux unavailable") + data = (optional_images / "animation.webp").read_bytes() + chunks, offset, frames = [], 12, 0 + while offset + 8 <= len(data): + size = int.from_bytes(data[offset + 4 : offset + 8], "little") + end = offset + 8 + size + (size & 1) + if data[offset : offset + 4] == b"ANMF": + frames += 1 + if frames < 2: + chunks.append(data[offset:end]) + offset = end + body = b"WEBP" + b"".join(chunks) + encoded = b"RIFF" + struct.pack("u2").tobytes()) + data = ( + b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", struct.pack(">IIBBBBB", 37, 29, 16, 6, 0, 0, 1)) + + chunk(b"IDAT", zlib.compress(raw)) + + chunk(b"IEND", b"") + ) + for mode in ("UNCHANGED", "RGB", "RGB_ALPHA", "GRAY", "GRAY_ALPHA"): + actual = decode_png(data, mode=mode, output_dtype="auto") + expected = as_numpy(oracle.decode_png(data, mode=mode, output_dtype="auto")) + np.testing.assert_array_equal(actual, expected) + + +@pytest.mark.parametrize("bits", [1, 2, 4]) +def test_low_bit_palette_png(oracle, bits): + from tensorcodec.decoders import decode_png + + image = Image.new("P", (37, 29)) + image.putpalette(list(range(256)) * 3) + image.putdata((np.arange(37 * 29) % (2**bits)).tolist()) + buffer = BytesIO() + image.save(buffer, format="PNG", bits=bits, transparency=0) + for mode in ("UNCHANGED", "RGB", "RGB_ALPHA", "GRAY", "GRAY_ALPHA"): + np.testing.assert_array_equal( + decode_png(buffer.getvalue(), mode=mode), as_numpy(oracle.decode_png(buffer.getvalue(), mode=mode)) + ) + + +@pytest.mark.parametrize("codec", ["WEBP", "GIF", "AVIF"]) +def test_image_animation_all_frames(oracle, codec): + from tensorcodec.decoders import decode_image + + rng = np.random.default_rng(42) + frames = [Image.fromarray(rng.integers(0, 256, (29, 37, 3), dtype=np.uint8)) for _ in range(3)] + output = BytesIO() + frames[0].save(output, format=codec, save_all=True, append_images=frames[1:], duration=100, max_threads=1) + actual = decode_image(output.getvalue()) + assert actual.shape == (3, 3, 29, 37) + np.testing.assert_array_equal(actual, as_numpy(oracle.decode_image(output.getvalue())))