From db2edc66e8e2f4a4809d9c279908d2ebc1ffe080 Mon Sep 17 00:00:00 2001 From: Suhwan Choi Date: Fri, 2 Oct 2026 03:44:53 +0000 Subject: [PATCH 1/5] Add TorchCodec-style CPU image decoding APIs --- .github/workflows/ci.yml | 6 +- .github/workflows/macos-wheels.yml | 2 +- .github/workflows/publish.yml | 2 +- README.md | 26 +- docs/compatibility.md | 45 ++- licenses/README.md | 4 + native/src/ffmpeg.rs | 179 ++++++++++ native/src/images.rs | 40 +++ native/src/lib.rs | 2 + pyproject.toml | 3 +- src/tensorcodec/decoders/__init__.py | 26 +- src/tensorcodec/decoders/_image_libraries.py | 150 ++++++++ src/tensorcodec/decoders/_images.py | 298 ++++++++++++++++ tests/resources/images/LICENSE.torchcodec | 28 ++ tests/resources/images/README.md | 8 + tests/resources/images/animated.heic | Bin 0 -> 799 bytes tests/resources/images/gradient_10bit.heic | Bin 0 -> 6330 bytes tests/resources/images/rgba.heic | Bin 0 -> 328919 bytes tests/test_images.py | 349 +++++++++++++++++++ 19 files changed, 1155 insertions(+), 13 deletions(-) create mode 100644 native/src/images.rs create mode 100644 src/tensorcodec/decoders/_image_libraries.py create mode 100644 src/tensorcodec/decoders/_images.py create mode 100644 tests/resources/images/LICENSE.torchcodec create mode 100644 tests/resources/images/README.md create mode 100644 tests/resources/images/animated.heic create mode 100644 tests/resources/images/gradient_10bit.heic create mode 100644 tests/resources/images/rgba.heic create mode 100644 tests/test_images.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74a6eea..cf10ab3 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 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' echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV" - name: Configure prebuilt FFmpeg @@ -60,6 +60,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..cfe6661 100644 --- a/.github/workflows/macos-wheels.yml +++ b/.github/workflows/macos-wheels.yml @@ -33,7 +33,7 @@ jobs: run: | brew install nasm pkg-config coreutils meson ninja 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" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 00b2543..14c3988 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -87,7 +87,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..cebe07a 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") +``` + +JPEG, PNG, WebP, GIF and single-stream AVIF use the bundled FFmpeg backend. +Animated WebP requires system `libwebpdemux`; HEIC/HEIF requires system `libheif`, +as it does in TorchCodec. These optional libraries are loaded only when needed. +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/animated WebP need optional system libraries | ✓ CPU; JPEG also CUDA | | Video / audio / image encoders | — | ✓ | | Clip samplers | — | ✓ | | Decoder transforms | ✓ Resize, CenterCrop, RandomCrop | ✓ | diff --git a/docs/compatibility.md b/docs/compatibility.md index 41ada19..471e6d5 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,10 +1,53 @@ # 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. +## 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. +- 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). It controls FFmpeg + worker threads; it is not a libavif setting in TensorCodec. + +JPEG, PNG, still WebP, GIF (including disposal/compositing) and single-stream AVIF +use FFmpeg, releasing the GIL during native decoding. Animated WebP uses optional +system `libwebpdemux`. HEIC uses optional system `libheif`, including high depth, +alpha and multiple images of equal shape, channel count and bit depth. Missing +optional libraries raise ImportError. These libraries are not bundled by this +change and do not add Python runtime dependencies. HEIC support also depends on +the decoders compiled into libheif. + +Animated PNG and multi-stream AVIF (including separate alpha planes) fail explicitly. +CMYK JPEG supports RGB/gray conversion, but `UNCHANGED` CMYK samples are unsupported. +No partial first-image fallback is used for unsupported image containers. +PNG native-value and HEIC high-depth fixtures are compared exactly. RGB conversion +is backend-dependent: the tested JPEG/AVIF cases allow up to 2 uint8 levels; +high-depth AVIF allows 257 uint16 levels (one 8-bit-equivalent level) versus libavif. +This is image color-conversion compatibility, not bit-exact parity with every +TorchCodec codec backend. Transparent GIF RGB values are compared where opaque; +alpha and RGB-mode background composition are checked separately. + +Image fixtures use known PNG sample values, independent FFmpeg/Pillow encodings, +and pinned upstream HEIC assets with their license. Pillow is test-only. + ## Public surface - `tensorcodec.Frame`, `FrameBatch`, `AudioSamples` diff --git a/licenses/README.md b/licenses/README.md index 664e922..ccf241c 100644 --- a/licenses/README.md +++ b/licenses/README.md @@ -21,3 +21,7 @@ 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 can optionally load the user's system libheif (HEIC) and +libwebpdemux (animated WebP); these are not bundled. The source distribution's +HEIC test assets retain TorchCodec's BSD license in `tests/resources/images/`. diff --git a/native/src/ffmpeg.rs b/native/src/ffmpeg.rs index a53a356..60d9c52 100644 --- a/native/src/ffmpeg.rs +++ b/native/src/ffmpeg.rs @@ -777,6 +777,178 @@ impl Decoder { } } + pub fn images(&mut self, requested_channels: usize) -> Result { + unsafe { + (*self.codec).err_recognition = + av::AV_EF_CRCCHECK | av::AV_EF_BITSTREAM | av::AV_EF_BUFFER | av::AV_EF_EXPLODE; + } + if unsafe { (*self.format).nb_streams } != 1 { + return Err(failure( + "multi-stream images (including separate alpha planes) are unsupported", + )); + } + let mut data = Vec::new(); + let mut layout = None; + let mut frames = 0; + while self.next()? { + unsafe { + let frame = &*self.frame; + let input: av::AVPixelFormat = std::mem::transmute(frame.format); + let descriptor = av::av_pix_fmt_desc_get(input); + if descriptor.is_null() || frame.width <= 0 || frame.height <= 0 { + return Err(failure("invalid image pixel layout")); + } + let descriptor = &*descriptor; + let depth = descriptor.comp[0].depth; + if !(1..=16).contains(&depth) { + return Err(failure("unsupported image bit depth")); + } + let channels = if requested_channels != 0 { + requested_channels + } else { + let gray = descriptor.nb_components <= 2 + && descriptor.flags & (av::AV_PIX_FMT_FLAG_PAL as u64) == 0; + let alpha = descriptor.flags & (av::AV_PIX_FMT_FLAG_ALPHA as u64) != 0; + (if gray { 1 } else { 3 }) + usize::from(alpha) + }; + let high = depth > 8; + let current = (frame.width as usize, frame.height as usize, channels, high); + if layout.is_some_and(|previous| previous != current) { + return Err(failure( + "image frames have different dimensions, channels or bit depths", + )); + } + layout = Some(current); + use av::AVPixelFormat::*; + let output = match (channels, high) { + (1, false) => AV_PIX_FMT_GRAY8, + (2, false) => AV_PIX_FMT_YA8, + (3, false) => AV_PIX_FMT_RGB24, + (4, false) => AV_PIX_FMT_RGBA, + (1, true) => AV_PIX_FMT_GRAY16LE, + (2, true) => AV_PIX_FMT_YA16LE, + (3, true) => AV_PIX_FMT_RGB48LE, + (4, true) => AV_PIX_FMT_RGBA64LE, + _ => return Err(failure("unsupported image channel count")), + }; + let config = (frame.width, frame.height, input as i32, output as i32); + if self.scale_config != Some(config) { + av::sws_freeContext(self.scale); + self.scale = av::sws_getContext( + frame.width, + frame.height, + input, + frame.width, + frame.height, + output, + 0, + ptr::null_mut(), + ptr::null_mut(), + ptr::null(), + ); + self.scale_config = Some(config); + } + if self.scale.is_null() { + return Err(failure("cannot initialize image color conversion")); + } + let mut inverse = ptr::null_mut(); + let mut table = ptr::null_mut(); + let ( + mut source_range, + mut destination_range, + mut brightness, + mut contrast, + mut saturation, + ) = (0, 0, 0, 0, 0); + check( + av::sws_getColorspaceDetails( + self.scale, + &mut inverse, + &mut source_range, + &mut table, + &mut destination_range, + &mut brightness, + &mut contrast, + &mut saturation, + ), + "read image color settings", + )?; + if frame.color_range != av::AVColorRange::AVCOL_RANGE_UNSPECIFIED { + source_range = + i32::from(frame.color_range == av::AVColorRange::AVCOL_RANGE_JPEG); + } + let coefficients = av::sws_getCoefficients(frame.colorspace as i32); + check( + av::sws_setColorspaceDetails( + self.scale, + coefficients, + source_range, + coefficients, + destination_range, + brightness, + contrast, + saturation, + ), + "configure image color conversion", + )?; + let stride = current + .0 + .checked_mul(channels) + .and_then(|n| n.checked_mul(if high { 2 } else { 1 })) + .filter(|&n| n <= i32::MAX as usize) + .ok_or_else(|| failure("image is too large"))?; + let length = stride + .checked_mul(current.1) + .ok_or_else(|| failure("image is too large"))?; + let start = data.len(); + let end = start + .checked_add(length) + .and_then(|n| n.checked_add(64)) + .ok_or_else(|| failure("image batch is too large"))?; + data.resize(end, 0); + let destination = [ + data.as_mut_ptr().add(start), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ]; + let lines = [stride as i32, 0, 0, 0]; + let rows = av::sws_scale( + self.scale, + frame.data.as_ptr() as *const *const u8, + frame.linesize.as_ptr(), + 0, + frame.height, + destination.as_ptr(), + lines.as_ptr(), + ); + if rows != frame.height { + return Err(failure("incomplete image color conversion")); + } + data.truncate(start + length); + frames += 1; + } + } + let (width, height, channels, high) = + layout.ok_or_else(|| failure("image contains no frames"))?; + let pixels = if high { + Pixels::U16( + data.chunks_exact(2) + .map(|p| u16::from_le_bytes([p[0], p[1]])) + .collect(), + ) + } else { + Pixels::U8(data) + }; + Ok(Images { + pixels, + frames, + width, + height, + channels, + }) + } + pub fn video( &mut self, request: VideoRequest, @@ -1330,6 +1502,13 @@ pub struct Video { pub height: usize, pub channels: usize, } +pub struct Images { + pub pixels: Pixels, + pub frames: usize, + pub width: usize, + pub height: usize, + pub channels: usize, +} pub struct Audio { pub data: Vec, pub samples: usize, diff --git a/native/src/images.rs b/native/src/images.rs new file mode 100644 index 0000000..a122a50 --- /dev/null +++ b/native/src/images.rs @@ -0,0 +1,40 @@ +//! Still/animated images use sequential decoding, without video timestamps or seeking. +use numpy::{ndarray::Array, IntoPyArray}; +use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::prelude::*; + +use crate::ffmpeg::{self, Pixels, Source}; + +#[pyfunction] +#[pyo3(signature = (data, channels=0, threads=1))] +pub fn decode_image( + py: Python<'_>, + data: &[u8], + channels: usize, + threads: i32, +) -> PyResult { + if channels > 4 || threads < 1 { + return Err(PyValueError::new_err( + "invalid image channels or thread count", + )); + } + let source = Source::Bytes(data.to_vec()); + let images = py + .allow_threads(|| ffmpeg::Decoder::open(source, false, None, threads)?.images(channels)) + .map_err(ffmpeg::Error::into_py)?; + let shape = (images.frames, images.height, images.width, images.channels); + macro_rules! array { + ($data:expr) => { + Array::from_shape_vec(shape, $data) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))? + .into_pyarray(py) + .into_any() + .unbind() + }; + } + Ok(match images.pixels { + Pixels::U8(data) => array!(data), + Pixels::U16(data) => array!(data), + Pixels::F32(_) => unreachable!(), + }) +} diff --git a/native/src/lib.rs b/native/src/lib.rs index 48a7951..eed163e 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,6 +1,7 @@ //! Only the FFmpeg boundary is native. Public API and playback planning live in Python. mod ffmpeg; +mod images; use numpy::{ndarray::Array, IntoPyArray}; use pyo3::class::gc::{PyTraverseError, PyVisit}; @@ -136,6 +137,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/pyproject.toml b/pyproject.toml index e3ce749..006bf58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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,6 +40,7 @@ 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" }, ] 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..dccb731 --- /dev/null +++ b/src/tensorcodec/decoders/_image_libraries.py @@ -0,0 +1,150 @@ +"""Optional system-library bindings for formats FFmpeg 7 cannot decode completely.""" + +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 _WebPData(c.Structure): + _fields_ = [("bytes", c.c_void_p), ("size", c.c_size_t)] + + +class _WebPInfo(c.Structure): + _fields_ = [(name, c.c_uint32) for name in ("width", "height", "loop", "background", "frames")] + [ + ("padding", c.c_uint32 * 4) + ] + + +def webp_animation(data): + library = _library("webpdemux") + new = _function(library, "WebPAnimDecoderNewInternal", c.c_void_p, c.POINTER(_WebPData), c.c_void_p, c.c_int) + info = _function(library, "WebPAnimDecoderGetInfo", c.c_int, c.c_void_p, c.POINTER(_WebPInfo)) + more = _function(library, "WebPAnimDecoderHasMoreFrames", c.c_int, c.c_void_p) + next_frame = _function( + library, "WebPAnimDecoderGetNext", c.c_int, c.c_void_p, c.POINTER(c.POINTER(c.c_uint8)), c.POINTER(c.c_int) + ) + delete = _function(library, "WebPAnimDecoderDelete", None, c.c_void_p) + buffer = c.create_string_buffer(data) + source = _WebPData(c.cast(buffer, c.c_void_p), len(data)) + decoder = new(c.byref(source), None, 0x0107) # Stable libwebp demux ABI; default output is RGBA. + if not decoder: + raise RuntimeError("cannot open animated WebP") + try: + metadata = _WebPInfo() + if not info(decoder, c.byref(metadata)) or not metadata.width or not metadata.height: + raise RuntimeError("invalid WebP canvas") + frames = [] + while more(decoder): + pixels, timestamp = c.POINTER(c.c_uint8)(), c.c_int() + if not next_frame(decoder, c.byref(pixels), c.byref(timestamp)) or not pixels: + raise RuntimeError("cannot decode WebP frame") + frame = np.ctypeslib.as_array(pixels, shape=(metadata.height * metadata.width * 4,)) + frames.append(frame.reshape(metadata.height, metadata.width, 4).copy()) + if len(frames) != metadata.frames or not frames: + raise RuntimeError("WebP frame count differs from its header") + return np.stack(frames) + finally: + delete(decoder) + + +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/_images.py b/src/tensorcodec/decoders/_images.py new file mode 100644 index 0000000..3995127 --- /dev/null +++ b/src/tensorcodec/decoders/_images.py @@ -0,0 +1,298 @@ +"""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 + + +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 _jpeg_components(data): + offset = 2 + while offset < len(data): + if data[offset] != 0xFF: + break + while offset < len(data) and data[offset] == 0xFF: + offset += 1 + if offset >= len(data): + break + marker, offset = data[offset], offset + 1 + if marker in (0xDA, 0xD9): + break + length = int.from_bytes(data[offset : offset + 2], "big") + if length < 2 or offset + length > len(data): + break + if marker in (0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF): + if length < 8: + break + return data[offset + 7] + offset += length + raise RuntimeError("JPEG has no valid frame header") + + +def _check_webp(data): + alpha, animated = False, False + offset = 12 + while offset + 8 <= len(data): + size = int.from_bytes(data[offset + 4 : offset + 8], "little") + if data[offset : offset + 4] in (b"ANIM", b"ANMF"): + animated = True + if offset + 8 + size > len(data): + raise RuntimeError("truncated WebP chunk") + if data[offset : offset + 4] == b"VP8X" and size >= 10: + alpha = bool(data[offset + 8] & 0x10) + elif data[offset : offset + 4] == b"VP8L" and size >= 5: + alpha = bool(int.from_bytes(data[offset + 9 : offset + 13], "little") & (1 << 28)) + offset += 8 + size + (size & 1) + return alpha, animated + + +def _gif_info(data): + if len(data) < 13: + raise RuntimeError("truncated GIF header") + table_size = 3 * (2 << (data[10] & 7)) if data[10] & 128 else 0 + background = data[13 + data[11] * 3 : 16 + data[11] * 3] if table_size else bytes(3) + offset, alpha = 13 + table_size, False + while offset < len(data): + kind, offset = data[offset], offset + 1 + if kind == 0x3B: + return alpha, tuple(background) + if kind == 0x21: + if offset + 2 > len(data): + break + if data[offset] == 0xF9 and data[offset + 1] == 4 and offset + 6 <= len(data): + alpha |= bool(data[offset + 2] & 1) + offset += 1 + elif kind == 0x2C: + if offset + 9 > len(data): + break + flags = data[offset + 8] + offset += 9 + (3 * (2 << (flags & 7)) if flags & 128 else 0) + 1 + else: + raise RuntimeError("invalid GIF block") + while offset < len(data) and data[offset]: + offset += 1 + data[offset] + offset += 1 + raise RuntimeError("truncated GIF data") + + +def _color(images, mode, codec): + channels = images.shape[-1] + if mode is ImageReadMode.UNCHANGED: + 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 + channels = _png_channels(data) if codec == "png" else 0 + if codec == "jpeg": + components = _jpeg_components(data) + if components == 4 and mode is ImageReadMode.UNCHANGED: + raise NotImplementedError("UNCHANGED CMYK JPEG output is unsupported; choose an RGB or gray mode") + if components == 4: + channels = 3 # FFmpeg's fourth decoded component is not image alpha. + if mode in (ImageReadMode.GRAY, ImageReadMode.GRAY_ALPHA): + channels = 1 + if codec == "heic": + from tensorcodec.decoders._image_libraries import heic + + images = heic(data, high_depth=dtype != np.uint8) + elif codec == "webp": + alpha, animated = _check_webp(data) + if animated: + from tensorcodec.decoders._image_libraries import webp_animation + + images = webp_animation(data) + else: + images = _decode(data, 4 if alpha else 3, threads) + if not alpha: + images = images[..., :3] + elif codec == "gif": + alpha, background = _gif_info(data) + images = _decode(data, 4, threads) + if mode in (ImageReadMode.RGB, ImageReadMode.GRAY): + images[..., :3] = np.where(images[..., 3:] == 0, np.array(background, np.uint8), images[..., :3]) + if not alpha: + images = images[..., :3] + else: + images = _decode(data, channels, threads) + 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 = images.transpose(0, 3, 1, 2) + return images[0] if len(images) == 1 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; animated WebP requires libwebpdemux. + Animated PNG and multi-stream AVIF are 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 require system 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 a single-stream AVIF to CHW/NCHW; num_threads controls FFmpeg 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 0000000000000000000000000000000000000000..4345bedd406da9b1df123db13a6ec0d3336b5e7f GIT binary patch literal 799 zcmZQzV30{GsVvAy&8%Pmg51nBLl8SRGck>Uf$>gmYDpqU90(LMQgVtQOa_L6%;XXn z8%Dcj=Hw@XI4%wh%nXbm78B465Df%LAP!I@2`&-|R0R^*4HroPa(y#1^MFcGq5Ote9Ym(c(A}AVWI-i9^vHtoT9wsoC=^gF9TZxP)H=R zxF8isgMb4RYb56+f?NyYg96b2N&_hdrvLweZUu)ggFr!MMJ7m)nS%r56^+b-+(Zx` z2!PSR+Q`Jr+QPsDrJ12L1EYFwN+Ku@<1`r8v+j`3N#6heU;7`nP3o7I`^_x|Vylv? z=l}n6d0YSg?)9!s(WTd8uSKR#i;P_Uf8)Cb`A_nX|9Ah9U;gq}-TalWs^%>R5lbKV z9L@0-*wn6nxhpHdUF)1i1JH(0s0|4~2Qe*LvAzEPiMi|l|NWZpedp=?`1tq)IWUl& zZvS^e@&Er#|CxX94?p^`CSLs5$36MdAVTVcTGBSvj!5U}FOQlUC{IlnaR3^efN=VM zgNE2bo5yp{7vK4A=P2MJ$O0tz=luWw#5abqI@)S;)x5y(Ah)hyJWwFR4kV=a&Hs18 k^8Y`3K25KnscW;Nu5E8L7Tu`hbV_EwpXSO&nbj7H0HZkGwg3PC literal 0 HcmV?d00001 diff --git a/tests/resources/images/gradient_10bit.heic b/tests/resources/images/gradient_10bit.heic new file mode 100644 index 0000000000000000000000000000000000000000..20cb8bc19b6674db8c846a159726df2d71267221 GIT binary patch literal 6330 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±<|k9ZC-NHv>eXzxf!7+qnV|@* z>Qru>m$>9D?0ol0^G}gHs%rhcTkKkj*b|JIIj^@4U#2|e(e+>ZCTB%5Fz|sJ=T1B3 z?5M>j8GnL0({D(asPLI8eLE*Av7OcZscryY^2NUXH>{XoG`}{8O2=-vB<2S zHG9^9{S9+(j;mBYr&8iGKR!R^R;Tdc6#F6V!ptF2_!nXya2kogX;Kw-9iM6(zaHka zJK7HHq>1Ig`P4jjkKTCrjF-w8(et)`QgoLr4{cvUy3TC`m|_SxJa2AmbqS<0>kd9CBR)xI&XKVb24P$ RQGB2QNI3k1Pl5Tx{{wc7o{#_l literal 0 HcmV?d00001 diff --git a/tests/resources/images/rgba.heic b/tests/resources/images/rgba.heic new file mode 100644 index 0000000000000000000000000000000000000000..6a2173dea00f0aa2eebd8936cbd53ee27cab97ba GIT binary patch literal 328919 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 z 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", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data)) + + 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 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) + animated = b"RIFF" + struct.pack(" Date: Fri, 2 Oct 2026 03:48:21 +0000 Subject: [PATCH 2/5] Preserve single-frame WebP animations and satisfy newer Clippy --- native/src/ffmpeg.rs | 6 ++++-- pyproject.toml | 2 +- src/tensorcodec/decoders/_images.py | 3 ++- tests/test_images.py | 24 ++++++++++++++++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/native/src/ffmpeg.rs b/native/src/ffmpeg.rs index 60d9c52..12791bc 100644 --- a/native/src/ffmpeg.rs +++ b/native/src/ffmpeg.rs @@ -933,8 +933,10 @@ impl Decoder { layout.ok_or_else(|| failure("image contains no frames"))?; let pixels = if high { Pixels::U16( - data.chunks_exact(2) - .map(|p| u16::from_le_bytes([p[0], p[1]])) + data.as_chunks::<2>() + .0 + .iter() + .map(|p| u16::from_le_bytes(*p)) .collect(), ) } else { diff --git a/pyproject.toml b/pyproject.toml index 006bf58..b7b906a 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/*"] diff --git a/src/tensorcodec/decoders/_images.py b/src/tensorcodec/decoders/_images.py index 3995127..0d3b773 100644 --- a/src/tensorcodec/decoders/_images.py +++ b/src/tensorcodec/decoders/_images.py @@ -242,7 +242,8 @@ def _image(source, codec, mode, output_dtype, threads=1): else: images = np.rint(images.astype(np.float32) / 257).clip(0, 255).astype(np.uint8) images = images.transpose(0, 3, 1, 2) - return images[0] if len(images) == 1 else images + 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): diff --git a/tests/test_images.py b/tests/test_images.py index 5b852b0..db52797 100644 --- a/tests/test_images.py +++ b/tests/test_images.py @@ -251,6 +251,30 @@ def test_heic_high_depth(oracle, optional_images, dtype): 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(" Date: Fri, 2 Oct 2026 03:58:55 +0000 Subject: [PATCH 3/5] Apply image orientation metadata without changing source pixels --- docs/compatibility.md | 3 + native/src/images.rs | 53 ++++-- .../decoders/_image_orientation.py | 153 ++++++++++++++++++ src/tensorcodec/decoders/_images.py | 6 +- tests/test_images.py | 17 ++ 5 files changed, 219 insertions(+), 13 deletions(-) create mode 100644 src/tensorcodec/decoders/_image_orientation.py diff --git a/docs/compatibility.md b/docs/compatibility.md index 471e6d5..d641d00 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -18,6 +18,9 @@ and `output_dtype=np.uint8`. One image is CHW; animated/multi-image output is NC - 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 EXIF orientation and AVIF primary-item rotation/mirroring are applied. + AVIF track display matrices support right-angle rotations; reflections fail explicitly. + 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, diff --git a/native/src/images.rs b/native/src/images.rs index a122a50..6a29f6d 100644 --- a/native/src/images.rs +++ b/native/src/images.rs @@ -1,17 +1,21 @@ //! Still/animated images use sequential decoding, without video timestamps or seeking. -use numpy::{ndarray::Array, IntoPyArray}; -use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use numpy::{ + ndarray::{Array, Axis}, + IntoPyArray, +}; +use pyo3::exceptions::{PyNotImplementedError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; use crate::ffmpeg::{self, Pixels, Source}; #[pyfunction] -#[pyo3(signature = (data, channels=0, threads=1))] +#[pyo3(signature = (data, channels=0, threads=1, apply_rotation=false))] pub fn decode_image( py: Python<'_>, data: &[u8], channels: usize, threads: i32, + apply_rotation: bool, ) -> PyResult { if channels > 4 || threads < 1 { return Err(PyValueError::new_err( @@ -19,18 +23,45 @@ pub fn decode_image( )); } let source = Source::Bytes(data.to_vec()); + let mut decoder = py + .allow_threads(|| ffmpeg::Decoder::open(source, false, None, threads)) + .map_err(ffmpeg::Error::into_py)?; + let turns = if apply_rotation { + let metadata = decoder.metadata(py, true)?; + let rotation = metadata + .get_item("rotation")? + .unwrap() + .extract::>()? + .unwrap_or(0.); + let turns = (rotation / 90.).round(); + if (rotation - turns * 90.).abs() > 1e-4 { + return Err(PyNotImplementedError::new_err( + "non-right-angle image rotation is unsupported", + )); + } + (turns as i32).rem_euclid(4) + } else { + 0 + }; let images = py - .allow_threads(|| ffmpeg::Decoder::open(source, false, None, threads)?.images(channels)) + .allow_threads(|| decoder.images(channels)) .map_err(ffmpeg::Error::into_py)?; let shape = (images.frames, images.height, images.width, images.channels); macro_rules! array { - ($data:expr) => { - Array::from_shape_vec(shape, $data) - .map_err(|e| PyRuntimeError::new_err(e.to_string()))? - .into_pyarray(py) - .into_any() - .unbind() - }; + ($data:expr) => {{ + let mut array = Array::from_shape_vec(shape, $data) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + if turns % 2 == 1 { + array.swap_axes(1, 2); + } + if turns == 1 || turns == 2 { + array.invert_axis(Axis(1)); + } + if turns == 2 || turns == 3 { + array.invert_axis(Axis(2)); + } + array.into_pyarray(py).into_any().unbind() + }}; } Ok(match images.pixels { Pixels::U8(data) => array!(data), diff --git a/src/tensorcodec/decoders/_image_orientation.py b/src/tensorcodec/decoders/_image_orientation.py new file mode 100644 index 0000000..f6f6021 --- /dev/null +++ b/src/tensorcodec/decoders/_image_orientation.py @@ -0,0 +1,153 @@ +"""JPEG/PNG EXIF and AVIF item orientation, without an image-library dependency.""" + +import numpy as np + + +def _boxes(data, start=0, end=None): + end = len(data) if end is None else end + while start < end: + if start + 8 > end: + raise RuntimeError("truncated AVIF box") + size, kind = int.from_bytes(data[start : start + 4], "big"), data[start + 4 : start + 8] + header = 8 + if size == 1: + if start + 16 > end: + raise RuntimeError("truncated extended AVIF box") + size, header = int.from_bytes(data[start + 8 : start + 16], "big"), 16 + elif size == 0: + size = end - start + if size < header or start + size > end: + raise RuntimeError("invalid AVIF box size") + yield kind, start + header, start + size + start += size + + +def _avif_orientation(data): + properties, associations, primary = [], [], None + for kind, begin, end in _boxes(data): + if kind != b"meta": + continue + for child, lo, hi in _boxes(data, begin + 4, end): + if child == b"pitm": + width = 2 if data[lo] == 0 else 4 + if lo + 4 + width > hi: + raise RuntimeError("truncated AVIF primary item") + primary = int.from_bytes(data[lo + 4 : lo + 4 + width], "big") + if child != b"iprp": + continue + for prop, p, q in _boxes(data, lo, hi): + if prop == b"ipco": + properties = list(_boxes(data, p, q)) + elif prop == b"ipma": + if p + 8 > q: + raise RuntimeError("truncated AVIF property associations") + width = 2 if data[p] == 0 else 4 + entry_width = 2 if int.from_bytes(data[p + 1 : p + 4], "big") & 1 else 1 + count, cursor = int.from_bytes(data[p + 4 : p + 8], "big"), p + 8 + for _ in range(count): + if cursor + width + 1 > q: + raise RuntimeError("truncated AVIF item association") + item = int.from_bytes(data[cursor : cursor + width], "big") + n = data[cursor + width] + cursor += width + 1 + if cursor + n * entry_width > q: + raise RuntimeError("truncated AVIF property indices") + indices = [ + int.from_bytes(data[i : i + entry_width], "big") & ((1 << (entry_width * 8 - 1)) - 1) + for i in range(cursor, cursor + n * entry_width, entry_width) + ] + associations.append((item, indices)) + cursor += n * entry_width + angle, axis = 0, None + for item, indices in associations: + if item != primary: + continue + for index in indices: + if index == 0: + continue + if index > len(properties): + raise RuntimeError("invalid AVIF property index") + kind, start, end = properties[index - 1] + if kind in (b"irot", b"imir"): + if start == end: + raise RuntimeError("empty AVIF orientation property") + if kind == b"irot": + angle = data[start] & 3 + else: + axis = data[start] & 1 + # ISO/IEC 23008-12 item rotation/mirror mapped to TIFF's eight orientations. + return ((1, 4, 2), (8, 5, 7), (3, 2, 4), (6, 7, 5))[angle][0 if axis is None else axis + 1] + + +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 == "avif": + return _avif_orientation(data) + if 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 index 0d3b773..597222c 100644 --- a/src/tensorcodec/decoders/_images.py +++ b/src/tensorcodec/decoders/_images.py @@ -6,6 +6,7 @@ import numpy as np from tensorcodec._native import decode_image as _decode +from tensorcodec.decoders._image_orientation import apply_orientation, orientation class ImageReadMode(Enum): @@ -203,6 +204,7 @@ def _image(source, codec, mode, output_dtype, threads=1): if codec is not None and found != codec: raise RuntimeError(f"expected {codec}, got {found}") codec = found + orient = orientation(data, codec) channels = _png_channels(data) if codec == "png" else 0 if codec == "jpeg": components = _jpeg_components(data) @@ -234,14 +236,14 @@ def _image(source, codec, mode, output_dtype, threads=1): if not alpha: images = images[..., :3] else: - images = _decode(data, channels, threads) + images = _decode(data, channels, threads, apply_rotation=codec == "avif" and orient == 1) 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 = images.transpose(0, 3, 1, 2) + 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 diff --git a/tests/test_images.py b/tests/test_images.py index db52797..b3897e4 100644 --- a/tests/test_images.py +++ b/tests/test_images.py @@ -371,3 +371,20 @@ def test_cmyk_jpeg_modes(oracle): assert (actual[-1] == 255).all() with pytest.raises(NotImplementedError, match="CMYK"): decode_jpeg(data, mode="UNCHANGED") + + +@pytest.mark.parametrize("codec", ["PNG", "JPEG", "AVIF"]) +@pytest.mark.parametrize("orientation", range(1, 9)) +def test_exif_orientation(oracle, codec, orientation): + from tensorcodec.decoders import decode_image + + pixels = np.arange(16 * 24 * 3, dtype=np.uint8).reshape(16, 24, 3) + image, output = Image.fromarray(pixels), BytesIO() + exif = Image.Exif() + exif[274] = orientation + image.save( + output, format=codec, exif=exif, quality=100, subsampling="4:4:4" if codec == "AVIF" else 0, max_threads=1 + ) + actual, expected = decode_image(output.getvalue()), as_numpy(oracle.decode_image(output.getvalue())) + assert actual.shape == expected.shape + np.testing.assert_allclose(actual.astype(int), expected.astype(int), atol=2, rtol=0) From dc53fe9c5b9919a5d2032297ed6c4d44b390fd2f Mon Sep 17 00:00:00 2001 From: Suhwan Choi Date: Fri, 2 Oct 2026 10:16:35 +0000 Subject: [PATCH 4/5] Use measured format-specific image backends and expand parity coverage --- .github/workflows/ci.yml | 9 +- .github/workflows/macos-wheels.yml | 4 +- .github/workflows/publish.yml | 7 +- README.md | 13 +- benchmarks/image_candidates/Cargo.lock | 496 +++++ benchmarks/image_candidates/Cargo.toml | 15 + benchmarks/image_candidates/README.md | 11 + benchmarks/image_candidates/baseline.json | 1838 +++++++++++++++ benchmarks/image_candidates/selected.json | 1982 +++++++++++++++++ benchmarks/image_candidates/src/lib.rs | 21 + .../image_candidates/turbo-baseline.json | 272 +++ benchmarks/image_decode.py | 148 ++ docs/compatibility.md | 56 +- docs/image-backends.md | 97 + licenses/README.md | 13 +- licenses/giflib.txt | 28 + licenses/libavif.txt | 387 ++++ licenses/libjpeg-ijg.txt | 260 +++ licenses/libjpeg-turbo.txt | 147 ++ licenses/libwebp.txt | 30 + licenses/libyuv.txt | 29 + licenses/png-rust.txt | 1692 ++++++++++++++ licenses/torchcodec.txt | 28 + native/Cargo.lock | 83 + native/Cargo.toml | 5 + native/build.rs | 27 + native/src/ffmpeg.rs | 182 +- native/src/image_codecs.c | 283 +++ native/src/images.rs | 145 +- native/src/lib.rs | 1 + native/src/png_image.rs | 146 ++ native/vendor/giflib/README | 30 + native/vendor/giflib/dgif_lib.c | 1313 +++++++++++ native/vendor/giflib/gif_hash.c | 129 ++ native/vendor/giflib/gif_hash.h | 43 + native/vendor/giflib/gif_lib.h | 292 +++ native/vendor/giflib/gif_lib_private.h | 73 + native/vendor/giflib/gifalloc.c | 426 ++++ native/vendor/giflib/openbsd-reallocarray.c | 74 + scripts/build_image_deps.sh | 48 + scripts/build_macos_wheel.sh | 2 + scripts/check_wheel_runtime.py | 29 +- src/tensorcodec/decoders/_image_libraries.py | 44 +- .../decoders/_image_orientation.py | 92 +- src/tensorcodec/decoders/_images.py | 115 +- tests/test_images.py | 154 +- 46 files changed, 10807 insertions(+), 512 deletions(-) create mode 100644 benchmarks/image_candidates/Cargo.lock create mode 100644 benchmarks/image_candidates/Cargo.toml create mode 100644 benchmarks/image_candidates/README.md create mode 100644 benchmarks/image_candidates/baseline.json create mode 100644 benchmarks/image_candidates/selected.json create mode 100644 benchmarks/image_candidates/src/lib.rs create mode 100644 benchmarks/image_candidates/turbo-baseline.json create mode 100644 benchmarks/image_decode.py create mode 100644 docs/image-backends.md create mode 100644 licenses/giflib.txt create mode 100644 licenses/libavif.txt create mode 100644 licenses/libjpeg-ijg.txt create mode 100644 licenses/libjpeg-turbo.txt create mode 100644 licenses/libwebp.txt create mode 100644 licenses/libyuv.txt create mode 100644 licenses/png-rust.txt create mode 100644 licenses/torchcodec.txt create mode 100644 native/build.rs create mode 100644 native/src/image_codecs.c create mode 100644 native/src/png_image.rs create mode 100644 native/vendor/giflib/README create mode 100644 native/vendor/giflib/dgif_lib.c create mode 100644 native/vendor/giflib/gif_hash.c create mode 100644 native/vendor/giflib/gif_hash.h create mode 100644 native/vendor/giflib/gif_lib.h create mode 100644 native/vendor/giflib/gif_lib_private.h create mode 100644 native/vendor/giflib/gifalloc.c create mode 100644 native/vendor/giflib/openbsd-reallocarray.c create mode 100644 scripts/build_image_deps.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf10ab3..20f70d8 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 --with libheif --with libwebp + 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' 'Pillow>=11.3' + .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,6 +49,11 @@ 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 diff --git a/.github/workflows/macos-wheels.yml b/.github/workflows/macos-wheels.yml index cfe6661..a1d4c45 100644 --- a/.github/workflows/macos-wheels.yml +++ b/.github/workflows/macos-wheels.yml @@ -31,7 +31,7 @@ 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 'Pillow>=11.3' echo "$PWD/.venv/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 14c3988..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() diff --git a/README.md b/README.md index cebe07a..59e55eb 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,9 @@ batch = decode_jpeg(["left.jpg", "right.jpg"]) depth = decode_png("depth.png", mode="UNCHANGED", output_dtype="auto") ``` -JPEG, PNG, WebP, GIF and single-stream AVIF use the bundled FFmpeg backend. -Animated WebP requires system `libwebpdemux`; HEIC/HEIF requires system `libheif`, -as it does in TorchCodec. These optional libraries are loaded only when needed. +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. @@ -98,7 +98,7 @@ Current source relative to TorchCodec 0.17.0; image functions are not in the 0.1 | --- | --- | --- | | Video decoder | △ CPU, SDR/HDR RGB | ✓ CPU / CUDA | | Audio decoder | ✓ CPU | ✓ CPU | -| Image decoders | △ CPU; HEIC/animated WebP need optional system libraries | ✓ CPU; JPEG also CUDA | +| Image decoders | △ CPU; HEIC needs optional system libheif | ✓ CPU; JPEG also CUDA | | Video / audio / image encoders | — | ✓ | | Clip samplers | — | ✓ | | Decoder transforms | ✓ Resize, CenterCrop, RandomCrop | ✓ | @@ -190,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..0db4ffb --- /dev/null +++ b/benchmarks/image_candidates/src/lib.rs @@ -0,0 +1,21 @@ +use numpy::{ndarray::Array, IntoPyArray}; +use pyo3::{prelude::*, exceptions::PyRuntimeError}; +#[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 d641d00..307777a 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -2,8 +2,8 @@ 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 @@ -18,8 +18,7 @@ and `output_dtype=np.uint8`. One image is CHW; animated/multi-image output is NC - 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 EXIF orientation and AVIF primary-item rotation/mirroring are applied. - AVIF track display matrices support right-angle rotations; reflections fail explicitly. +- 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: @@ -27,29 +26,38 @@ and `output_dtype=np.uint8`. One image is CHW; animated/multi-image output is NC 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). It controls FFmpeg - worker threads; it is not a libavif setting in TensorCodec. - -JPEG, PNG, still WebP, GIF (including disposal/compositing) and single-stream AVIF -use FFmpeg, releasing the GIL during native decoding. Animated WebP uses optional -system `libwebpdemux`. HEIC uses optional system `libheif`, including high depth, -alpha and multiple images of equal shape, channel count and bit depth. Missing -optional libraries raise ImportError. These libraries are not bundled by this -change and do not add Python runtime dependencies. HEIC support also depends on -the decoders compiled into libheif. - -Animated PNG and multi-stream AVIF (including separate alpha planes) fail explicitly. -CMYK JPEG supports RGB/gray conversion, but `UNCHANGED` CMYK samples are unsupported. -No partial first-image fallback is used for unsupported image containers. -PNG native-value and HEIC high-depth fixtures are compared exactly. RGB conversion -is backend-dependent: the tested JPEG/AVIF cases allow up to 2 uint8 levels; -high-depth AVIF allows 257 uint16 levels (one 8-bit-equivalent level) versus libavif. -This is image color-conversion compatibility, not bit-exact parity with every -TorchCodec codec backend. Transparent GIF RGB values are compared where opaque; -alpha and RGB-mode background composition are checked separately. +- 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 ccf241c..a32f77f 100644 --- a/licenses/README.md +++ b/licenses/README.md @@ -20,8 +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. -Image decoding can optionally load the user's system libheif (HEIC) and -libwebpdemux (animated WebP); these are not bundled. The source distribution's +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 12791bc..e4eea41 100644 --- a/native/src/ffmpeg.rs +++ b/native/src/ffmpeg.rs @@ -777,180 +777,6 @@ impl Decoder { } } - pub fn images(&mut self, requested_channels: usize) -> Result { - unsafe { - (*self.codec).err_recognition = - av::AV_EF_CRCCHECK | av::AV_EF_BITSTREAM | av::AV_EF_BUFFER | av::AV_EF_EXPLODE; - } - if unsafe { (*self.format).nb_streams } != 1 { - return Err(failure( - "multi-stream images (including separate alpha planes) are unsupported", - )); - } - let mut data = Vec::new(); - let mut layout = None; - let mut frames = 0; - while self.next()? { - unsafe { - let frame = &*self.frame; - let input: av::AVPixelFormat = std::mem::transmute(frame.format); - let descriptor = av::av_pix_fmt_desc_get(input); - if descriptor.is_null() || frame.width <= 0 || frame.height <= 0 { - return Err(failure("invalid image pixel layout")); - } - let descriptor = &*descriptor; - let depth = descriptor.comp[0].depth; - if !(1..=16).contains(&depth) { - return Err(failure("unsupported image bit depth")); - } - let channels = if requested_channels != 0 { - requested_channels - } else { - let gray = descriptor.nb_components <= 2 - && descriptor.flags & (av::AV_PIX_FMT_FLAG_PAL as u64) == 0; - let alpha = descriptor.flags & (av::AV_PIX_FMT_FLAG_ALPHA as u64) != 0; - (if gray { 1 } else { 3 }) + usize::from(alpha) - }; - let high = depth > 8; - let current = (frame.width as usize, frame.height as usize, channels, high); - if layout.is_some_and(|previous| previous != current) { - return Err(failure( - "image frames have different dimensions, channels or bit depths", - )); - } - layout = Some(current); - use av::AVPixelFormat::*; - let output = match (channels, high) { - (1, false) => AV_PIX_FMT_GRAY8, - (2, false) => AV_PIX_FMT_YA8, - (3, false) => AV_PIX_FMT_RGB24, - (4, false) => AV_PIX_FMT_RGBA, - (1, true) => AV_PIX_FMT_GRAY16LE, - (2, true) => AV_PIX_FMT_YA16LE, - (3, true) => AV_PIX_FMT_RGB48LE, - (4, true) => AV_PIX_FMT_RGBA64LE, - _ => return Err(failure("unsupported image channel count")), - }; - let config = (frame.width, frame.height, input as i32, output as i32); - if self.scale_config != Some(config) { - av::sws_freeContext(self.scale); - self.scale = av::sws_getContext( - frame.width, - frame.height, - input, - frame.width, - frame.height, - output, - 0, - ptr::null_mut(), - ptr::null_mut(), - ptr::null(), - ); - self.scale_config = Some(config); - } - if self.scale.is_null() { - return Err(failure("cannot initialize image color conversion")); - } - let mut inverse = ptr::null_mut(); - let mut table = ptr::null_mut(); - let ( - mut source_range, - mut destination_range, - mut brightness, - mut contrast, - mut saturation, - ) = (0, 0, 0, 0, 0); - check( - av::sws_getColorspaceDetails( - self.scale, - &mut inverse, - &mut source_range, - &mut table, - &mut destination_range, - &mut brightness, - &mut contrast, - &mut saturation, - ), - "read image color settings", - )?; - if frame.color_range != av::AVColorRange::AVCOL_RANGE_UNSPECIFIED { - source_range = - i32::from(frame.color_range == av::AVColorRange::AVCOL_RANGE_JPEG); - } - let coefficients = av::sws_getCoefficients(frame.colorspace as i32); - check( - av::sws_setColorspaceDetails( - self.scale, - coefficients, - source_range, - coefficients, - destination_range, - brightness, - contrast, - saturation, - ), - "configure image color conversion", - )?; - let stride = current - .0 - .checked_mul(channels) - .and_then(|n| n.checked_mul(if high { 2 } else { 1 })) - .filter(|&n| n <= i32::MAX as usize) - .ok_or_else(|| failure("image is too large"))?; - let length = stride - .checked_mul(current.1) - .ok_or_else(|| failure("image is too large"))?; - let start = data.len(); - let end = start - .checked_add(length) - .and_then(|n| n.checked_add(64)) - .ok_or_else(|| failure("image batch is too large"))?; - data.resize(end, 0); - let destination = [ - data.as_mut_ptr().add(start), - ptr::null_mut(), - ptr::null_mut(), - ptr::null_mut(), - ]; - let lines = [stride as i32, 0, 0, 0]; - let rows = av::sws_scale( - self.scale, - frame.data.as_ptr() as *const *const u8, - frame.linesize.as_ptr(), - 0, - frame.height, - destination.as_ptr(), - lines.as_ptr(), - ); - if rows != frame.height { - return Err(failure("incomplete image color conversion")); - } - data.truncate(start + length); - frames += 1; - } - } - let (width, height, channels, high) = - layout.ok_or_else(|| failure("image contains no frames"))?; - let pixels = if high { - Pixels::U16( - data.as_chunks::<2>() - .0 - .iter() - .map(|p| u16::from_le_bytes(*p)) - .collect(), - ) - } else { - Pixels::U8(data) - }; - Ok(Images { - pixels, - frames, - width, - height, - channels, - }) - } - pub fn video( &mut self, request: VideoRequest, @@ -1504,13 +1330,7 @@ pub struct Video { pub height: usize, pub channels: usize, } -pub struct Images { - pub pixels: Pixels, - pub frames: usize, - pub width: usize, - 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 index 6a29f6d..f62ac9b 100644 --- a/native/src/images.rs +++ b/native/src/images.rs @@ -1,71 +1,102 @@ -//! Still/animated images use sequential decoding, without video timestamps or seeking. -use numpy::{ - ndarray::{Array, Axis}, - IntoPyArray, -}; -use pyo3::exceptions::{PyNotImplementedError, PyRuntimeError, PyValueError}; +//! 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}; -use crate::ffmpeg::{self, Pixels, Source}; +#[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] -#[pyo3(signature = (data, channels=0, threads=1, apply_rotation=false))] pub fn decode_image( py: Python<'_>, data: &[u8], - channels: usize, + codec: i32, + mode: i32, + depth: i32, threads: i32, - apply_rotation: bool, -) -> PyResult { - if channels > 4 || threads < 1 { - return Err(PyValueError::new_err( - "invalid image channels or thread count", - )); +) -> 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")); } - let source = Source::Bytes(data.to_vec()); - let mut decoder = py - .allow_threads(|| ffmpeg::Decoder::open(source, false, None, threads)) - .map_err(ffmpeg::Error::into_py)?; - let turns = if apply_rotation { - let metadata = decoder.metadata(py, true)?; - let rotation = metadata - .get_item("rotation")? - .unwrap() - .extract::>()? - .unwrap_or(0.); - let turns = (rotation / 90.).round(); - if (rotation - turns * 90.).abs() > 1e-4 { - return Err(PyNotImplementedError::new_err( - "non-right-angle image rotation is unsupported", + 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(), )); } - (turns as i32).rem_euclid(4) + 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 { - 0 + Array::from_shape_vec(shape, pixels) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))? + .into_pyarray(py) + .into_any() + .unbind() }; - let images = py - .allow_threads(|| decoder.images(channels)) - .map_err(ffmpeg::Error::into_py)?; - let shape = (images.frames, images.height, images.width, images.channels); - macro_rules! array { - ($data:expr) => {{ - let mut array = Array::from_shape_vec(shape, $data) - .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; - if turns % 2 == 1 { - array.swap_axes(1, 2); - } - if turns == 1 || turns == 2 { - array.invert_axis(Axis(1)); - } - if turns == 2 || turns == 3 { - array.invert_axis(Axis(2)); - } - array.into_pyarray(py).into_any().unbind() - }}; - } - Ok(match images.pixels { - Pixels::U8(data) => array!(data), - Pixels::U16(data) => array!(data), - Pixels::F32(_) => unreachable!(), - }) + Ok((array, orientation, animated)) } diff --git a/native/src/lib.rs b/native/src/lib.rs index eed163e..26fc181 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -2,6 +2,7 @@ mod ffmpeg; mod images; +mod png_image; use numpy::{ndarray::Array, IntoPyArray}; use pyo3::class::gc::{PyTraverseError, PyVisit}; 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/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/_image_libraries.py b/src/tensorcodec/decoders/_image_libraries.py index dccb731..58c0f7a 100644 --- a/src/tensorcodec/decoders/_image_libraries.py +++ b/src/tensorcodec/decoders/_image_libraries.py @@ -1,4 +1,4 @@ -"""Optional system-library bindings for formats FFmpeg 7 cannot decode completely.""" +"""Optional system libheif binding for HEIC decoding.""" import ctypes as c from ctypes.util import find_library @@ -24,48 +24,6 @@ def _function(library, name, result, *arguments): return function -class _WebPData(c.Structure): - _fields_ = [("bytes", c.c_void_p), ("size", c.c_size_t)] - - -class _WebPInfo(c.Structure): - _fields_ = [(name, c.c_uint32) for name in ("width", "height", "loop", "background", "frames")] + [ - ("padding", c.c_uint32 * 4) - ] - - -def webp_animation(data): - library = _library("webpdemux") - new = _function(library, "WebPAnimDecoderNewInternal", c.c_void_p, c.POINTER(_WebPData), c.c_void_p, c.c_int) - info = _function(library, "WebPAnimDecoderGetInfo", c.c_int, c.c_void_p, c.POINTER(_WebPInfo)) - more = _function(library, "WebPAnimDecoderHasMoreFrames", c.c_int, c.c_void_p) - next_frame = _function( - library, "WebPAnimDecoderGetNext", c.c_int, c.c_void_p, c.POINTER(c.POINTER(c.c_uint8)), c.POINTER(c.c_int) - ) - delete = _function(library, "WebPAnimDecoderDelete", None, c.c_void_p) - buffer = c.create_string_buffer(data) - source = _WebPData(c.cast(buffer, c.c_void_p), len(data)) - decoder = new(c.byref(source), None, 0x0107) # Stable libwebp demux ABI; default output is RGBA. - if not decoder: - raise RuntimeError("cannot open animated WebP") - try: - metadata = _WebPInfo() - if not info(decoder, c.byref(metadata)) or not metadata.width or not metadata.height: - raise RuntimeError("invalid WebP canvas") - frames = [] - while more(decoder): - pixels, timestamp = c.POINTER(c.c_uint8)(), c.c_int() - if not next_frame(decoder, c.byref(pixels), c.byref(timestamp)) or not pixels: - raise RuntimeError("cannot decode WebP frame") - frame = np.ctypeslib.as_array(pixels, shape=(metadata.height * metadata.width * 4,)) - frames.append(frame.reshape(metadata.height, metadata.width, 4).copy()) - if len(frames) != metadata.frames or not frames: - raise RuntimeError("WebP frame count differs from its header") - return np.stack(frames) - finally: - delete(decoder) - - class _HeifError(c.Structure): _fields_ = [("code", c.c_int), ("subcode", c.c_int), ("message", c.c_char_p)] diff --git a/src/tensorcodec/decoders/_image_orientation.py b/src/tensorcodec/decoders/_image_orientation.py index f6f6021..ac0cbd1 100644 --- a/src/tensorcodec/decoders/_image_orientation.py +++ b/src/tensorcodec/decoders/_image_orientation.py @@ -1,84 +1,8 @@ -"""JPEG/PNG EXIF and AVIF item orientation, without an image-library dependency.""" +"""JPEG, PNG and WebP EXIF orientation.""" import numpy as np -def _boxes(data, start=0, end=None): - end = len(data) if end is None else end - while start < end: - if start + 8 > end: - raise RuntimeError("truncated AVIF box") - size, kind = int.from_bytes(data[start : start + 4], "big"), data[start + 4 : start + 8] - header = 8 - if size == 1: - if start + 16 > end: - raise RuntimeError("truncated extended AVIF box") - size, header = int.from_bytes(data[start + 8 : start + 16], "big"), 16 - elif size == 0: - size = end - start - if size < header or start + size > end: - raise RuntimeError("invalid AVIF box size") - yield kind, start + header, start + size - start += size - - -def _avif_orientation(data): - properties, associations, primary = [], [], None - for kind, begin, end in _boxes(data): - if kind != b"meta": - continue - for child, lo, hi in _boxes(data, begin + 4, end): - if child == b"pitm": - width = 2 if data[lo] == 0 else 4 - if lo + 4 + width > hi: - raise RuntimeError("truncated AVIF primary item") - primary = int.from_bytes(data[lo + 4 : lo + 4 + width], "big") - if child != b"iprp": - continue - for prop, p, q in _boxes(data, lo, hi): - if prop == b"ipco": - properties = list(_boxes(data, p, q)) - elif prop == b"ipma": - if p + 8 > q: - raise RuntimeError("truncated AVIF property associations") - width = 2 if data[p] == 0 else 4 - entry_width = 2 if int.from_bytes(data[p + 1 : p + 4], "big") & 1 else 1 - count, cursor = int.from_bytes(data[p + 4 : p + 8], "big"), p + 8 - for _ in range(count): - if cursor + width + 1 > q: - raise RuntimeError("truncated AVIF item association") - item = int.from_bytes(data[cursor : cursor + width], "big") - n = data[cursor + width] - cursor += width + 1 - if cursor + n * entry_width > q: - raise RuntimeError("truncated AVIF property indices") - indices = [ - int.from_bytes(data[i : i + entry_width], "big") & ((1 << (entry_width * 8 - 1)) - 1) - for i in range(cursor, cursor + n * entry_width, entry_width) - ] - associations.append((item, indices)) - cursor += n * entry_width - angle, axis = 0, None - for item, indices in associations: - if item != primary: - continue - for index in indices: - if index == 0: - continue - if index > len(properties): - raise RuntimeError("invalid AVIF property index") - kind, start, end = properties[index - 1] - if kind in (b"irot", b"imir"): - if start == end: - raise RuntimeError("empty AVIF orientation property") - if kind == b"irot": - angle = data[start] & 3 - else: - axis = data[start] & 1 - # ISO/IEC 23008-12 item rotation/mirror mapped to TIFF's eight orientations. - return ((1, 4, 2), (8, 5, 7), (3, 2, 4), (6, 7, 5))[angle][0 if axis is None else axis + 1] - - def _tiff_orientation(data): if len(data) < 8 or data[:2] not in (b"II", b"MM"): return 1 @@ -105,9 +29,17 @@ def _tiff_orientation(data): def orientation(data, codec): - if codec == "avif": - return _avif_orientation(data) - if codec == "png": + 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") diff --git a/src/tensorcodec/decoders/_images.py b/src/tensorcodec/decoders/_images.py index 597222c..fe3361c 100644 --- a/src/tensorcodec/decoders/_images.py +++ b/src/tensorcodec/decoders/_images.py @@ -94,79 +94,12 @@ def _png_channels(data): return channels -def _jpeg_components(data): - offset = 2 - while offset < len(data): - if data[offset] != 0xFF: - break - while offset < len(data) and data[offset] == 0xFF: - offset += 1 - if offset >= len(data): - break - marker, offset = data[offset], offset + 1 - if marker in (0xDA, 0xD9): - break - length = int.from_bytes(data[offset : offset + 2], "big") - if length < 2 or offset + length > len(data): - break - if marker in (0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF): - if length < 8: - break - return data[offset + 7] - offset += length - raise RuntimeError("JPEG has no valid frame header") - - -def _check_webp(data): - alpha, animated = False, False - offset = 12 - while offset + 8 <= len(data): - size = int.from_bytes(data[offset + 4 : offset + 8], "little") - if data[offset : offset + 4] in (b"ANIM", b"ANMF"): - animated = True - if offset + 8 + size > len(data): - raise RuntimeError("truncated WebP chunk") - if data[offset : offset + 4] == b"VP8X" and size >= 10: - alpha = bool(data[offset + 8] & 0x10) - elif data[offset : offset + 4] == b"VP8L" and size >= 5: - alpha = bool(int.from_bytes(data[offset + 9 : offset + 13], "little") & (1 << 28)) - offset += 8 + size + (size & 1) - return alpha, animated - - -def _gif_info(data): - if len(data) < 13: - raise RuntimeError("truncated GIF header") - table_size = 3 * (2 << (data[10] & 7)) if data[10] & 128 else 0 - background = data[13 + data[11] * 3 : 16 + data[11] * 3] if table_size else bytes(3) - offset, alpha = 13 + table_size, False - while offset < len(data): - kind, offset = data[offset], offset + 1 - if kind == 0x3B: - return alpha, tuple(background) - if kind == 0x21: - if offset + 2 > len(data): - break - if data[offset] == 0xF9 and data[offset + 1] == 4 and offset + 6 <= len(data): - alpha |= bool(data[offset + 2] & 1) - offset += 1 - elif kind == 0x2C: - if offset + 9 > len(data): - break - flags = data[offset + 8] - offset += 9 + (3 * (2 << (flags & 7)) if flags & 128 else 0) + 1 - else: - raise RuntimeError("invalid GIF block") - while offset < len(data) and data[offset]: - offset += 1 + data[offset] - offset += 1 - raise RuntimeError("truncated GIF data") - - 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) @@ -205,38 +138,20 @@ def _image(source, codec, mode, output_dtype, threads=1): raise RuntimeError(f"expected {codec}, got {found}") codec = found orient = orientation(data, codec) - channels = _png_channels(data) if codec == "png" else 0 - if codec == "jpeg": - components = _jpeg_components(data) - if components == 4 and mode is ImageReadMode.UNCHANGED: - raise NotImplementedError("UNCHANGED CMYK JPEG output is unsupported; choose an RGB or gray mode") - if components == 4: - channels = 3 # FFmpeg's fourth decoded component is not image alpha. - if mode in (ImageReadMode.GRAY, ImageReadMode.GRAY_ALPHA): - channels = 1 + 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) - elif codec == "webp": - alpha, animated = _check_webp(data) - if animated: - from tensorcodec.decoders._image_libraries import webp_animation - - images = webp_animation(data) - else: - images = _decode(data, 4 if alpha else 3, threads) - if not alpha: - images = images[..., :3] - elif codec == "gif": - alpha, background = _gif_info(data) - images = _decode(data, 4, threads) - if mode in (ImageReadMode.RGB, ImageReadMode.GRAY): - images[..., :3] = np.where(images[..., 3:] == 0, np.array(background, np.uint8), images[..., :3]) - if not alpha: - images = images[..., :3] else: - images = _decode(data, channels, threads, apply_rotation=codec == "avif" and orient == 1) + 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: @@ -254,8 +169,8 @@ def decode_image(source, *, mode="RGB", output_dtype=np.uint8): 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; animated WebP requires libwebpdemux. - Animated PNG and multi-stream AVIF are unsupported. + HEIC requires system libheif. Other image codecs are bundled in wheels. + Animated PNG is unsupported. """ return _image(source, None, mode, output_dtype) @@ -280,7 +195,7 @@ def decode_png(source, *, mode="RGB", output_dtype=np.uint8): def decode_webp(source, *, mode="RGB", output_dtype=np.uint8): - """Decode WebP to CHW/NCHW. Animations require system libwebpdemux.""" + """Decode WebP to CHW/NCHW. Animations are composited with libwebpdemux.""" return _image(source, "webp", mode, output_dtype) @@ -290,7 +205,7 @@ def decode_gif(source, *, mode="RGB", output_dtype=np.uint8): def decode_avif(source, *, mode="RGB", output_dtype=np.uint8, num_threads=1): - """Decode a single-stream AVIF to CHW/NCHW; num_threads controls FFmpeg workers.""" + """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) diff --git a/tests/test_images.py b/tests/test_images.py index b3897e4..0407406 100644 --- a/tests/test_images.py +++ b/tests/test_images.py @@ -13,14 +13,15 @@ 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] - def chunk(kind, data): - return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data)) - 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)) @@ -33,6 +34,8 @@ def chunk(kind, data): def dtype_for(backend, name): + if name == "auto": + return name if backend.__name__.startswith("torchcodec"): import torch @@ -190,9 +193,6 @@ def test_optional_libraries_report_missing_dependencies(monkeypatch): heic = struct.pack(">I", 24) + b"ftypheic" + b"\0" * 4 + b"mif1heic" with pytest.raises(ImportError, match="libheif"): decode_image(heic) - animated = 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()))) From 448e8941fd618cfcffa8623f6339805ef2439e5a Mon Sep 17 00:00:00 2001 From: Suhwan Choi Date: Fri, 2 Oct 2026 10:19:25 +0000 Subject: [PATCH 5/5] Package reproducible image backend experiments in source distributions --- .github/workflows/ci.yml | 4 ++-- benchmarks/image_candidates/src/lib.rs | 32 ++++++++++++++++++++------ pyproject.toml | 1 + 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20f70d8..562fdeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,8 +56,8 @@ jobs: 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 diff --git a/benchmarks/image_candidates/src/lib.rs b/benchmarks/image_candidates/src/lib.rs index 0db4ffb..fda9a7d 100644 --- a/benchmarks/image_candidates/src/lib.rs +++ b/benchmarks/image_candidates/src/lib.rs @@ -1,17 +1,35 @@ use numpy::{ndarray::Array, IntoPyArray}; -use pyo3::{prelude::*, exceptions::PyRuntimeError}; +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())) + 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()) + 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()) + 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<()> { diff --git a/pyproject.toml b/pyproject.toml index b7b906a..fc77316 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ include = [ { 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]