Full Code of kornelski/cavif-rs for AI

main e9b45424cf1a cached
14 files
62.5 KB
17.2k tokens
66 symbols
1 requests
Download .txt
Repository: kornelski/cavif-rs
Branch: main
Commit: e9b45424cf1a
Files: 14
Total size: 62.5 KB

Directory structure:
gitextract_35ej9mke/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .rustfmt.toml
├── Cargo.toml
├── LICENSE
├── README.md
├── ravif/
│   ├── Cargo.toml
│   ├── README.md
│   └── src/
│       ├── av1encoder.rs
│       ├── dirtyalpha.rs
│       ├── error.rs
│       └── lib.rs
├── src/
│   └── main.rs
└── tests/
    └── stdio.rs

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: Rust

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: dtolnay/rust-toolchain@stable
    - uses: ilammy/setup-nasm@v1
    - name: Tests
      run: cargo test --verbose --all --all-targets
    - name: Check semver
      uses: obi1kenobi/cargo-semver-checks-action@v2
      with:
        package: ravif


================================================
FILE: .gitignore
================================================
target/
Cargo.lock


================================================
FILE: .rustfmt.toml
================================================
array_width = 120
binop_separator = "Back"
chain_width = 120
comment_width = 222
condense_wildcard_suffixes = true
disable_all_formatting = true
edition = "2024"
enum_discrim_align_threshold = 5
fn_call_width = 120
fn_params_layout = "Compressed"
fn_single_line = false
force_explicit_abi = false
format_code_in_doc_comments = true
imports_granularity = "Module"
imports_layout = "Horizontal"
match_block_trailing_comma = true
max_width = 160
overflow_delimited_expr = true
reorder_impl_items = true
single_line_if_else_max_width = 150
struct_lit_width = 40
use_field_init_shorthand = true
use_small_heuristics = "Max"
use_try_shorthand = true
where_single_line = true
wrap_comments = true


================================================
FILE: Cargo.toml
================================================
[package]
name = "cavif"
description = "Encodes images in AVIF format (image2avif converter) using a pure-Rust encoder."
version = "1.7.0"
authors = ["Kornel Lesiński <kornel@geekhood.net>"]
edition = "2024"
license = "BSD-3-Clause"
readme = "README.md"
keywords = ["avif", "png2avif", "jpeg2avif", "convert", "av1"]
categories = ["command-line-utilities", "multimedia::images", "multimedia::encoding"]
homepage = "https://lib.rs/crates/cavif"
repository = "https://github.com/kornelski/cavif-rs"
include = ["README.md", "LICENSE", "/src/*.rs"]
rust-version = "1.85"

[dependencies]
clap = { version = "4.5.40", default-features = false, features = ["color", "suggestions", "wrap_help", "std", "cargo"] }
cocoa_image = { version = "1.1.0", optional = true }
imgref = "1.11.0"
load_image = "3.2.1"
ravif = { version = "0.13", path = "./ravif", default-features = false, features = ["threading"] }
rayon = "1.11.0"
rgb = { version = "0.8.52", default-features = false }

[features]
default = ["asm", "static"]
asm = ["ravif/asm"]
static = ["load_image/lcms2-static"]

[profile.dev]
opt-level = 1
debug = 1

[profile.release]
opt-level = 3
panic = "abort"
debug = false
lto = true
strip = true

[profile.dev.package."*"]
opt-level = 2

[dev-dependencies]
avif-parse = "1.4.0"

[badges]
maintenance = { status = "actively-developed" }

[workspace]
members = ["ravif"]

[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
rustdoc-args = ["--generate-link-to-definition"]


================================================
FILE: LICENSE
================================================
BSD 3-Clause License

Copyright (c) 2020, Kornel
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.

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.


================================================
FILE: README.md
================================================
# `cavif` — PNG/JPEG to AVIF converter

Encoder/converter for AVIF images. Based on [`rav1e`](https://lib.rs/crates/rav1e) and [`avif-serialize`](https://lib.rs/crates/avif-serialize) via the [`ravif`](https://lib.rs/crates/ravif) crate, which makes it an almost pure-Rust tool (it uses C LCMS2 for color profiles).

## Installation

➡️ **[Download the latest release](https://github.com/kornelski/cavif/releases)** ⬅️

The pre-built zip includes a portable static executable, with no dependencies, that runs on any Linux distro. It also includes executables for macOS and Windows.

## Usage

Run in a terminal (hint: you don't need to type the path, terminals accept file drag'n'drop)

```bash
cavif image.png
```

It makes `image.avif`. You can adjust quality (it's in 1-100 scale):

```bash
cavif --quality 60 image.png
```

### Advanced usage

You can also specify multiple images. Encoding is multi-threaded, so the more, the better!

```text
cavif [OPTIONS] IMAGES...
```

 * `--quality=n` — Quality from 1 (worst) to 100 (best), the default value is 80. The numbers are only a rough approximation of JPEG's quality scale. [Beware when comparing codecs](https://kornel.ski/faircomparison). There is no lossless compression support, 100 just gives unreasonably bloated files.
 * `--speed=n` — Encoding speed between 1 (best, but slowest) and 10 (fastest, but a blurry mess), the default value is 4. Speeds 1 and 2 are unbelievably slow, but make files ~3-5% smaller. Speeds 7 and above degrade compression significantly, and are not recommended.
 * `--overwrite` — Replace files if there's `.avif` already. By default the existing files are left untouched.
 * `-o path` — Write images to this path (instead of `same-name.avif`). If multiple input files are specified, it's interpreted as a directory.
 * `--quiet` — Don't print anything during conversion.

There are additional options that tweak AVIF color space. The defaults in `cavif` are chosen to be the best, so use these options only when you know it's necessary:

 * `--dirty-alpha` — Preserve RGB values of fully transparent pixels (not recommended). By default irrelevant color of transparent pixels is cleared to avoid wasting space.
 * `--color=rgb` — Encode using RGB instead of YCbCr color space. Makes colors closer to lossless, but makes files larger. Use only if you need to avoid even smallest color shifts.
 * `--depth=8` — Encode using 8-bit color depth instead of 10-bit. This results in a slightly worse quality/compression ratio, but is more compatible.

## Compatibility

Images [work in all modern browsers](https://caniuse.com/avif).

* Chrome 85+ desktop,
* Chrome on Android 12,
* Firefox 91,
* Safari iOS 16/macOS Ventura.

### Known incompatibilities

* Windows' preview and very old versions of android are reported to show pink line at the right edge. This is probably a bug in an old AVIF decoder they use.
* Windows' preview doesn't seem to support 10-bit deep images. Use `--depth=8` when encoding if this is a problem.

## Building

To build it from source you need Rust 1.67 or later, preferably via [rustup](https://rustup.rs).

Then run in a terminal:

```bash
rustup update
cargo install cavif
```


================================================
FILE: ravif/Cargo.toml
================================================
[package]
name = "ravif"
description = "rav1e-based pure Rust library for encoding images in AVIF format (powers the `cavif` tool)"
version = "0.13.0"
authors = ["Kornel Lesiński <kornel@geekhood.net>"]
edition = "2024"
license = "BSD-3-Clause"
readme = "README.md"
keywords = ["avif", "convert", "av1", "rav1f", "cav1f"]
categories = ["multimedia::images", "multimedia::encoding"]
homepage = "https://lib.rs/crates/ravif"
repository = "https://github.com/kornelski/cavif-rs"
include = ["README.md", "LICENSE", "Cargo.toml", "/src/*.rs"]
rust-version = "1.85"

[dependencies]
avif-serialize = "0.8.6"
imgref = "1.12.0"
rav1e = { version = "0.8.1", default-features = false }
rayon = { version = "1.11.0", optional = true }
rgb = { version = "0.8.52", default-features = false }
loop9 = "0.1.5"
quick-error = "2.0.1"

[target.'cfg(target = "wasm32-unknown-unknown")'.dependencies]
rav1e = { version = "0.8", default-features = false, features = ["wasm"] }

[features]
default = ["asm", "threading"]
asm = ["rav1e/asm"]
threading = ["dep:rayon", "rav1e/threading"]

[profile.release]
lto = true

[profile.dev.package."*"]
debug = false
opt-level = 2

[dev-dependencies]
avif-parse = "1.4.0"

[package.metadata.release]
tag = false


================================================
FILE: ravif/README.md
================================================
# `ravif` — Pure Rust library for AVIF image encoding

Encoder for AVIF images. Based on [`rav1e`](https://lib.rs/crates/rav1e) and [`avif-serialize`](https://lib.rs/crates/avif-serialize).

The API is just a single `encode_rgba()` function call that spits an AVIF image.

This library powers the [`cavif`](https://lib.rs/crates/cavif) encoder. It has an encoding configuration specifically tuned for still images, and gives better quality/performance than stock `rav1e`.


================================================
FILE: ravif/src/av1encoder.rs
================================================
#![allow(deprecated)]
use std::borrow::Cow;
use crate::dirtyalpha::blurred_dirty_alpha;
use crate::error::Error;
#[cfg(not(feature = "threading"))]
use crate::rayoff as rayon;
use imgref::{Img, ImgVec};
use rav1e::prelude::*;
use rgb::{RGB8, RGBA8};

/// For [`Encoder::with_internal_color_model`]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ColorModel {
    /// Standard color model for photographic content. Usually the best choice.
    /// This library always uses full-resolution color (4:4:4).
    /// This library will automatically choose between BT.601 or BT.709.
    YCbCr,
    /// RGB channels are encoded without color space transformation.
    /// Usually results in larger file sizes, and is less compatible than `YCbCr`.
    /// Use only if the content really makes use of RGB, e.g. anaglyph images or RGB subpixel anti-aliasing.
    RGB,
}

/// Handling of color channels in transparent images. For [`Encoder::with_alpha_color_mode`]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum AlphaColorMode {
    /// Use unassociated alpha channel and leave color channels unchanged, even if there's redundant color data in transparent areas.
    UnassociatedDirty,
    /// Use unassociated alpha channel, but set color channels of transparent areas to a solid color to eliminate invisible data and improve compression.
    UnassociatedClean,
    /// Store color channels of transparent images in premultiplied form.
    /// This requires support for premultiplied alpha in AVIF decoders.
    ///
    /// It may reduce file sizes due to clearing of fully-transparent pixels, but
    /// may also increase file sizes due to creation of new edges in the color channels.
    ///
    /// Note that this is only internal detail for the AVIF file.
    /// It does not change meaning of `RGBA` in this library — it's always unassociated.
    Premultiplied,
}

#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
pub enum BitDepth {
    Eight,
    Ten,
    /// Same as `Ten`
    #[default]
    Auto,
}

/// The newly-created image file + extra info FYI
#[non_exhaustive]
#[derive(Clone)]
pub struct EncodedImage {
    /// AVIF (HEIF+AV1) encoded image data
    pub avif_file: Vec<u8>,
    /// FYI: number of bytes of AV1 payload used for the color
    pub color_byte_size: usize,
    /// FYI: number of bytes of AV1 payload used for the alpha channel
    pub alpha_byte_size: usize,
}

/// Encoder config builder
///
/// The lifetime is relevant only for [`Encoder::with_exif()`]. Use `Encoder<'static>` if Rust complains.
#[derive(Debug, Clone)]
pub struct Encoder<'exif_slice> {
    /// 0-255 scale
    quantizer: u8,
    /// 0-255 scale
    alpha_quantizer: u8,
    /// rav1e preset 1 (slow) 10 (fast but crappy)
    speed: u8,
    /// True if RGBA input has already been premultiplied. It inserts appropriate metadata.
    premultiplied_alpha: bool,
    /// Which pixel format to use in AVIF file. RGB tends to give larger files.
    color_model: ColorModel,
    /// How many threads should be used (0 = match core count), None - use global rayon thread pool
    threads: Option<usize>,
    /// [`AlphaColorMode`]
    alpha_color_mode: AlphaColorMode,
    /// 8 or 10
    output_depth: BitDepth,
    /// Dropped into MPEG infe BOX
    exif: Option<Cow<'exif_slice, [u8]>>,
}

impl<'exif_slice> Default for Encoder<'exif_slice> {
    fn default() -> Self {
        Self {
           quantizer: quality_to_quantizer(80.),
           alpha_quantizer: quality_to_quantizer(80.),
           speed: 5,
           output_depth: BitDepth::default(),
           premultiplied_alpha: false,
           color_model: ColorModel::YCbCr,
           threads: None,
           exif: None,
           alpha_color_mode: AlphaColorMode::UnassociatedClean,
       }
    }
}

/// Builder methods
impl<'exif_slice> Encoder<'exif_slice> {
    /// Start here
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Quality `1..=100`. Panics if out of range.
    #[inline(always)]
    #[track_caller]
    #[must_use]
    pub fn with_quality(mut self, quality: f32) -> Self {
        assert!(quality >= 1. && quality <= 100.);
        self.quantizer = quality_to_quantizer(quality);
        self
    }

    #[doc(hidden)]
    #[deprecated(note = "Renamed to with_bit_depth")]
    #[must_use]
    pub fn with_depth(self, depth: Option<u8>) -> Self {
        self.with_bit_depth(depth.map(|d| if d >= 10 { BitDepth::Ten } else { BitDepth::Eight }).unwrap_or(BitDepth::Auto))
    }

    /// Internal precision to use in the encoded AV1 data, for both color and alpha. 10-bit depth works best, even for 8-bit inputs/outputs.
    ///
    /// Use 8-bit depth only as a workaround for decoders that need it.
    ///
    /// This setting does not affect pixel inputs for this library.
    #[inline(always)]
    #[must_use]
    pub fn with_bit_depth(mut self, depth: BitDepth) -> Self {
        self.output_depth = depth;
        self
    }

    /// Quality for the alpha channel only. `1..=100`. Panics if out of range.
    #[inline(always)]
    #[track_caller]
    #[must_use]
    pub fn with_alpha_quality(mut self, quality: f32) -> Self {
        assert!(quality >= 1. && quality <= 100.);
        self.alpha_quantizer = quality_to_quantizer(quality);
        self
    }

    /// * 1 = very very slow, but max compression.
    /// * 10 = quick, but larger file sizes and lower quality.
    ///
    /// Panics if outside `1..=10`.
    #[inline(always)]
    #[track_caller]
    #[must_use]
    pub fn with_speed(mut self, speed: u8) -> Self {
        assert!(speed >= 1 && speed <= 10);
        self.speed = speed;
        self
    }

    /// Changes how color channels are stored in the image. The default is YCbCr.
    ///
    /// Note that this is only internal detail for the AVIF file, and doesn't
    /// change color model of inputs to encode functions.
    #[inline(always)]
    #[must_use]
    pub fn with_internal_color_model(mut self, color_model: ColorModel) -> Self {
        self.color_model = color_model;
        self
    }

    #[doc(hidden)]
    #[deprecated = "Renamed to `with_internal_color_model()`"]
    #[must_use]
    pub fn with_internal_color_space(self, color_model: ColorModel) -> Self {
        self.with_internal_color_model(color_model)
    }

    /// Configures `rayon` thread pool size.
    /// The default `None` is to use all threads in the default `rayon` thread pool.
    #[inline(always)]
    #[track_caller]
    #[must_use]
    pub fn with_num_threads(mut self, num_threads: Option<usize>) -> Self {
        assert!(num_threads.is_none_or(|n| n > 0));
        self.threads = num_threads;
        self
    }

    /// Configure handling of color channels in transparent images
    ///
    /// Note that this doesn't affect input format for this library,
    /// which for RGBA is always uncorrelated alpha.
    #[inline(always)]
    #[must_use]
    pub fn with_alpha_color_mode(mut self, mode: AlphaColorMode) -> Self {
        self.alpha_color_mode = mode;
        self.premultiplied_alpha = mode == AlphaColorMode::Premultiplied;
        self
    }

    /// Embedded into AVIF file as-is
    ///
    /// The data can be `Vec<u8>`, or `&[u8]` if the encoder instance doesn't leave its scope.
    pub fn with_exif(mut self, exif_data: impl Into<Cow<'exif_slice, [u8]>>) -> Self {
        self.set_exif(exif_data);
        self
    }

    /// Embedded into AVIF file as-is
    ///
    /// The data can be `Vec<u8>`, or `&[u8]` if the encoder instance doesn't leave its scope.
    pub fn set_exif(&mut self, exif_data: impl Into<Cow<'exif_slice, [u8]>>) {
        self.exif = Some(exif_data.into());
    }
}

/// Once done with config, call one of the `encode_*` functions
impl Encoder<'_> {
    /// Make a new AVIF image from RGBA pixels (non-premultiplied, alpha last)
    ///
    /// Make the `Img` for the `buffer` like this:
    ///
    /// ```rust,ignore
    /// Img::new(&pixels_rgba[..], width, height)
    /// ```
    ///
    /// If you have pixels as `u8` slice, then use the `rgb` crate, and do:
    ///
    /// ```rust,ignore
    /// use rgb::ComponentSlice;
    /// let pixels_rgba = pixels_u8.as_rgba();
    /// ```
    ///
    /// If all pixels are opaque, the alpha channel will be left out automatically.
    ///
    /// This function takes 8-bit inputs, but will generate an AVIF file using 10-bit depth.
    ///
    /// returns AVIF file with info about sizes about AV1 payload.
    pub fn encode_rgba(&self, in_buffer: Img<&[rgb::RGBA<u8>]>) -> Result<EncodedImage, Error> {
        let new_alpha = self.convert_alpha_8bit(in_buffer);
        let buffer = new_alpha.as_ref().map(|b| b.as_ref()).unwrap_or(in_buffer);
        let use_alpha = buffer.pixels().any(|px| px.a != 255);
        if !use_alpha {
            return self.encode_rgb_internal_from_8bit(buffer.width(), buffer.height(), buffer.pixels().map(|px| px.rgb()));
        }

        let width = buffer.width();
        let height = buffer.height();
        let matrix_coefficients = match self.color_model {
            ColorModel::YCbCr => MatrixCoefficients::BT601,
            ColorModel::RGB => MatrixCoefficients::Identity,
        };
        match self.output_depth {
            BitDepth::Eight => {
                let planes = buffer.pixels().map(|px| match self.color_model {
                    ColorModel::YCbCr => rgb_to_8_bit_ycbcr(px.rgb(), BT601).into(),
                    ColorModel::RGB => rgb_to_8_bit_gbr(px.rgb()).into(),
                });
                let alpha = buffer.pixels().map(|px| px.a);
                self.encode_raw_planes_8_bit(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients)
            },
            BitDepth::Ten | BitDepth::Auto => {
                let planes = buffer.pixels().map(|px| match self.color_model {
                    ColorModel::YCbCr => rgb_to_10_bit_ycbcr(px.rgb(), BT601).into(),
                    ColorModel::RGB => rgb_to_10_bit_gbr(px.rgb()).into(),
                });
                let alpha = buffer.pixels().map(|px| to_ten(px.a));
                self.encode_raw_planes_10_bit(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients)
            },
        }
    }

    fn convert_alpha_8bit(&self, in_buffer: Img<&[RGBA8]>) -> Option<ImgVec<RGBA8>> {
        match self.alpha_color_mode {
            AlphaColorMode::UnassociatedDirty => None,
            AlphaColorMode::UnassociatedClean => blurred_dirty_alpha(in_buffer),
            AlphaColorMode::Premultiplied => {
                let prem = in_buffer.pixels()
                    .map(|px| {
                        if px.a == 0 || px.a == 255 {
                            RGBA8::default()
                        } else {
                            RGBA8::new(
                                (u16::from(px.r) * 255 / u16::from(px.a)) as u8,
                                (u16::from(px.g) * 255 / u16::from(px.a)) as u8,
                                (u16::from(px.b) * 255 / u16::from(px.a)) as u8,
                                px.a,
                            )
                        }
                    })
                    .collect();
                Some(ImgVec::new(prem, in_buffer.width(), in_buffer.height()))
            },
        }
    }

    /// Make a new AVIF image from RGB pixels
    ///
    /// Make the `Img` for the `buffer` like this:
    ///
    /// ```rust,ignore
    /// Img::new(&pixels_rgb[..], width, height)
    /// ```
    ///
    /// If you have pixels as `u8` slice, then first do:
    ///
    /// ```rust,ignore
    /// use rgb::ComponentSlice;
    /// let pixels_rgb = pixels_u8.as_rgb();
    /// ```
    ///
    /// returns AVIF file, size of color metadata
    #[inline]
    pub fn encode_rgb(&self, buffer: Img<&[RGB8]>) -> Result<EncodedImage, Error> {
        self.encode_rgb_internal_from_8bit(buffer.width(), buffer.height(), buffer.pixels())
    }

    fn encode_rgb_internal_from_8bit(&self, width: usize, height: usize, pixels: impl Iterator<Item = RGB8> + Send + Sync) -> Result<EncodedImage, Error> {
        let matrix_coefficients = match self.color_model {
            ColorModel::YCbCr => MatrixCoefficients::BT601,
            ColorModel::RGB => MatrixCoefficients::Identity,
        };

        match self.output_depth {
            BitDepth::Eight => {
                let planes = pixels.map(|px| {
                    let (y, u, v) = match self.color_model {
                        ColorModel::YCbCr => rgb_to_8_bit_ycbcr(px, BT601),
                        ColorModel::RGB => rgb_to_8_bit_gbr(px),
                    };
                    [y, u, v]
                });
                self.encode_raw_planes_8_bit(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients)
            },
            BitDepth::Ten | BitDepth::Auto => {
                let planes = pixels.map(|px| {
                    let (y, u, v) = match self.color_model {
                        ColorModel::YCbCr => rgb_to_10_bit_ycbcr(px, BT601),
                        ColorModel::RGB => rgb_to_10_bit_gbr(px),
                    };
                    [y, u, v]
                });
                self.encode_raw_planes_10_bit(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients)
            },
        }
    }

    /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`,
    /// with sRGB transfer characteristics and color primaries.
    ///
    /// Alpha always uses full range. Chroma subsampling is not supported, and it's a bad idea for AVIF anyway.
    /// If there's no alpha, use `None::<[_; 0]>`.
    ///
    /// `color_pixel_range` should be `PixelRange::Full` to avoid worsening already small 8-bit dynamic range.
    /// Support for limited range may be removed in the future.
    ///
    /// If `AlphaColorMode::Premultiplied` has been set, the alpha pixels must be premultiplied.
    /// `AlphaColorMode::UnassociatedClean` has no effect in this function, and is equivalent to `AlphaColorMode::UnassociatedDirty`.
    ///
    /// returns AVIF file, size of color metadata, size of alpha metadata overhead
    #[inline]
    pub fn encode_raw_planes_8_bit(
        &self, width: usize, height: usize,
        planes: impl IntoIterator<Item = [u8; 3]> + Send,
        alpha: Option<impl IntoIterator<Item = u8> + Send>,
        color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients,
    ) -> Result<EncodedImage, Error> {
        self.encode_raw_planes_internal(width, height, planes, alpha, color_pixel_range, matrix_coefficients, 8)
    }

    /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`,
    /// with sRGB transfer characteristics and color primaries.
    ///
    /// The pixels are 10-bit (values `0.=1023`) in host's native endian.
    ///
    /// Alpha always uses full range. Chroma subsampling is not supported, and it's a bad idea for AVIF anyway.
    /// If there's no alpha, use `None::<[_; 0]>`.
    ///
    /// `color_pixel_range` should be `PixelRange::Full`. Support for limited range may be removed in the future.
    ///
    /// If `AlphaColorMode::Premultiplied` has been set, the alpha pixels must be premultiplied.
    /// `AlphaColorMode::UnassociatedClean` has no effect in this function, and is equivalent to `AlphaColorMode::UnassociatedDirty`.
    ///
    /// returns AVIF file, size of color metadata, size of alpha metadata overhead
    #[inline]
    pub fn encode_raw_planes_10_bit(
        &self, width: usize, height: usize,
        planes: impl IntoIterator<Item = [u16; 3]> + Send,
        alpha: Option<impl IntoIterator<Item = u16> + Send>,
        color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients,
    ) -> Result<EncodedImage, Error> {
        self.encode_raw_planes_internal(width, height, planes, alpha, color_pixel_range, matrix_coefficients, 10)
    }

    #[inline(never)]
    fn encode_raw_planes_internal<P: rav1e::Pixel + Default>(
        &self, width: usize, height: usize,
        planes: impl IntoIterator<Item = [P; 3]> + Send,
        alpha: Option<impl IntoIterator<Item = P> + Send>,
        color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients,
        input_pixels_bit_depth: u8,
    ) -> Result<EncodedImage, Error> {
        let color_description = Some(ColorDescription {
            transfer_characteristics: TransferCharacteristics::SRGB,
            color_primaries: ColorPrimaries::BT709, // sRGB-compatible
            matrix_coefficients,
        });

        let threads = self.threads.map(|threads| {
            if threads > 0 { threads } else { rayon::current_num_threads() }
        });

        let encode_color = move || {
            encode_to_av1::<P>(
                &Av1EncodeConfig {
                    width,
                    height,
                    bit_depth: input_pixels_bit_depth.into(),
                    quantizer: self.quantizer.into(),
                    speed: SpeedTweaks::from_my_preset(self.speed, self.quantizer),
                    threads,
                    pixel_range: color_pixel_range,
                    chroma_sampling: ChromaSampling::Cs444,
                    color_description,
                },
                move |frame| init_frame_3(width, height, planes, frame),
            )
        };
        let encode_alpha = move || {
            alpha.map(|alpha| {
                encode_to_av1::<P>(
                    &Av1EncodeConfig {
                        width,
                        height,
                        bit_depth: input_pixels_bit_depth.into(),
                        quantizer: self.alpha_quantizer.into(),
                        speed: SpeedTweaks::from_my_preset(self.speed, self.alpha_quantizer),
                        threads,
                        pixel_range: PixelRange::Full,
                        chroma_sampling: ChromaSampling::Cs400,
                        color_description: None,
                    },
                    |frame| init_frame_1(width, height, alpha, frame),
                )
            })
        };
        #[cfg(all(target_arch = "wasm32", not(target_feature = "atomics")))]
        let (color, alpha) = (encode_color(), encode_alpha());
        #[cfg(not(all(target_arch = "wasm32", not(target_feature = "atomics"))))]
        let (color, alpha) = rayon::join(encode_color, encode_alpha);
        let (color, alpha) = (color?, alpha.transpose()?);

        let mut serializer_config = avif_serialize::Aviffy::new();
        serializer_config
            .matrix_coefficients(match matrix_coefficients {
                MatrixCoefficients::Identity => avif_serialize::constants::MatrixCoefficients::Rgb,
                MatrixCoefficients::BT709 => avif_serialize::constants::MatrixCoefficients::Bt709,
                MatrixCoefficients::Unspecified => avif_serialize::constants::MatrixCoefficients::Unspecified,
                MatrixCoefficients::BT601 => avif_serialize::constants::MatrixCoefficients::Bt601,
                MatrixCoefficients::YCgCo => avif_serialize::constants::MatrixCoefficients::Ycgco,
                MatrixCoefficients::BT2020NCL => avif_serialize::constants::MatrixCoefficients::Bt2020Ncl,
                MatrixCoefficients::BT2020CL => avif_serialize::constants::MatrixCoefficients::Bt2020Cl,
                _ => return Err(Error::Unsupported("matrix coefficients")),
            })
            .premultiplied_alpha(self.premultiplied_alpha);
        if let Some(exif) = &self.exif {
            serializer_config.set_exif(exif.to_vec());
        }
        let avif_file = serializer_config.to_vec(&color, alpha.as_deref(), width as u32, height as u32, input_pixels_bit_depth);
        let color_byte_size = color.len();
        let alpha_byte_size = alpha.as_ref().map_or(0, |a| a.len());

        Ok(EncodedImage {
            avif_file, color_byte_size, alpha_byte_size,
        })
    }
}

/// Native endian
#[inline(always)]
fn to_ten(x: u8) -> u16 {
    (u16::from(x) << 2) | (u16::from(x) >> 6)
}

/// Native endian
#[inline(always)]
fn rgb_to_10_bit_gbr(px: rgb::RGB<u8>) -> (u16, u16, u16) {
    (to_ten(px.g), to_ten(px.b), to_ten(px.r))
}

#[inline(always)]
fn rgb_to_8_bit_gbr(px: rgb::RGB<u8>) -> (u8, u8, u8) {
    (px.g, px.b, px.r)
}

// const REC709: [f32; 3] = [0.2126, 0.7152, 0.0722];
const BT601: [f32; 3] = [0.2990, 0.5870, 0.1140];

#[inline(always)]
fn rgb_to_ycbcr(px: rgb::RGB<u8>, depth: u8, matrix: [f32; 3]) -> (f32, f32, f32) {
    let max_value = ((1 << depth) - 1) as f32;
    let scale = max_value / 255.;
    let shift = (max_value * 0.5).round();
    let y = (scale * matrix[2]).mul_add(f32::from(px.b), (scale * matrix[0]).mul_add(f32::from(px.r), scale * matrix[1] * f32::from(px.g)));
    let cb = f32::from(px.b).mul_add(scale, -y).mul_add(0.5 / (1. - matrix[2]), shift);
    let cr = f32::from(px.r).mul_add(scale, -y).mul_add(0.5 / (1. - matrix[0]), shift);
    (y.round(), cb.round(), cr.round())
}

#[inline(always)]
fn rgb_to_10_bit_ycbcr(px: rgb::RGB<u8>, matrix: [f32; 3]) -> (u16, u16, u16) {
    let (y, u, v) = rgb_to_ycbcr(px, 10, matrix);
    (y as u16, u as u16, v as u16)
}

#[inline(always)]
fn rgb_to_8_bit_ycbcr(px: rgb::RGB<u8>, matrix: [f32; 3]) -> (u8, u8, u8) {
    let (y, u, v) = rgb_to_ycbcr(px, 8, matrix);
    (y as u8, u as u8, v as u8)
}

fn quality_to_quantizer(quality: f32) -> u8 {
    let q = quality / 100.;
    let x = if q >= 0.82 { (1. - q) * 2.6 } else if q > 0.25 { q.mul_add(-0.5, 1. - 0.125) } else { 1. - q };
    (x * 255.).round() as u8
}

#[derive(Debug, Copy, Clone)]
struct SpeedTweaks {
    pub speed_preset: u8,

    pub fast_deblock: Option<bool>,
    pub reduced_tx_set: Option<bool>,
    pub tx_domain_distortion: Option<bool>,
    pub tx_domain_rate: Option<bool>,
    pub encode_bottomup: Option<bool>,
    pub rdo_tx_decision: Option<bool>,
    pub cdef: Option<bool>,
    /// loop restoration filter
    pub lrf: Option<bool>,
    pub sgr_complexity_full: Option<bool>,
    pub use_satd_subpel: Option<bool>,
    pub inter_tx_split: Option<bool>,
    pub fine_directional_intra: Option<bool>,
    pub complex_prediction_modes: Option<bool>,
    pub partition_range: Option<(u8, u8)>,
    pub min_tile_size: u16,
}

impl SpeedTweaks {
    pub fn from_my_preset(speed: u8, quantizer: u8) -> Self {
        let low_quality = quantizer < quality_to_quantizer(55.);
        let high_quality = quantizer > quality_to_quantizer(80.);
        let max_block_size = if high_quality { 16 } else { 64 };

        Self {
            speed_preset: speed,

            partition_range: Some(match speed {
                0 => (4, 64.min(max_block_size)),
                1 if low_quality => (4, 64.min(max_block_size)),
                2 if low_quality => (4, 32.min(max_block_size)),
                1..=4 => (4, 16),
                5..=8 => (8, 16),
                _ => (16, 16),
            }),

            complex_prediction_modes: Some(speed <= 1), // 2x-3x slower, 2% better
            sgr_complexity_full: Some(speed <= 2), // 15% slower, barely improves anything -/+1%

            encode_bottomup: Some(speed <= 2), // may be costly (+60%), may even backfire

            // big blocks disabled at 3

            // these two are together?
            rdo_tx_decision: Some(speed <= 4 && !high_quality), // it tends to blur subtle textures
            reduced_tx_set: Some(speed == 4 || speed >= 9), // It interacts with tx_domain_distortion too?

            // 4px blocks disabled at 5

            fine_directional_intra: Some(speed <= 6),
            fast_deblock: Some(speed >= 7 && !high_quality), // mixed bag?

            // 8px blocks disabled at 8
            lrf: Some(low_quality && speed <= 8), // hardly any help for hi-q images. recovers some q at low quality
            cdef: Some(low_quality && speed <= 9), // hardly any help for hi-q images. recovers some q at low quality

            inter_tx_split: Some(speed >= 9), // mixed bag even when it works, and it backfires if not used together with reduced_tx_set
            tx_domain_rate: Some(speed >= 10), // 20% faster, but also 10% larger files!

            tx_domain_distortion: None, // very mixed bag, sometimes helps speed sometimes it doesn't
            use_satd_subpel: Some(false), // doesn't make sense
            min_tile_size: match speed {
                0 => 4096,
                1 => 2048,
                2 => 1024,
                3 => 512,
                4 => 256,
                _ => 128,
            } * if high_quality { 2 } else { 1 },
        }
    }

    pub(crate) fn speed_settings(&self) -> SpeedSettings {
        let mut speed_settings = SpeedSettings::from_preset(self.speed_preset);

        speed_settings.multiref = false;
        speed_settings.rdo_lookahead_frames = 1;
        speed_settings.scene_detection_mode = SceneDetectionSpeed::None;
        speed_settings.motion.include_near_mvs = false;

        if let Some(v) = self.fast_deblock { speed_settings.fast_deblock = v; }
        if let Some(v) = self.reduced_tx_set { speed_settings.transform.reduced_tx_set = v; }
        if let Some(v) = self.tx_domain_distortion { speed_settings.transform.tx_domain_distortion = v; }
        if let Some(v) = self.tx_domain_rate { speed_settings.transform.tx_domain_rate = v; }
        if let Some(v) = self.encode_bottomup { speed_settings.partition.encode_bottomup = v; }
        if let Some(v) = self.rdo_tx_decision { speed_settings.transform.rdo_tx_decision = v; }
        if let Some(v) = self.cdef { speed_settings.cdef = v; }
        if let Some(v) = self.lrf { speed_settings.lrf = v; }
        if let Some(v) = self.inter_tx_split { speed_settings.transform.enable_inter_tx_split = v; }
        if let Some(v) = self.sgr_complexity_full { speed_settings.sgr_complexity = if v { SGRComplexityLevel::Full } else { SGRComplexityLevel::Reduced } }
        if let Some(v) = self.use_satd_subpel { speed_settings.motion.use_satd_subpel = v; }
        if let Some(v) = self.fine_directional_intra { speed_settings.prediction.fine_directional_intra = v; }
        if let Some(v) = self.complex_prediction_modes { speed_settings.prediction.prediction_modes = if v { PredictionModesSetting::ComplexAll } else { PredictionModesSetting::Simple} }
        if let Some((min, max)) = self.partition_range {
            debug_assert!(min <= max);
            fn sz(s: u8) -> BlockSize {
                match s {
                    4 => BlockSize::BLOCK_4X4,
                    8 => BlockSize::BLOCK_8X8,
                    16 => BlockSize::BLOCK_16X16,
                    32 => BlockSize::BLOCK_32X32,
                    64 => BlockSize::BLOCK_64X64,
                    128 => BlockSize::BLOCK_128X128,
                    _ => panic!("bad size {s}"),
                }
            }
            speed_settings.partition.partition_range = PartitionRange::new(sz(min), sz(max));
        }

        speed_settings
    }
}

struct Av1EncodeConfig {
    pub width: usize,
    pub height: usize,
    pub bit_depth: usize,
    pub quantizer: usize,
    pub speed: SpeedTweaks,
    /// 0 means num_cpus
    pub threads: Option<usize>,
    pub pixel_range: PixelRange,
    pub chroma_sampling: ChromaSampling,
    pub color_description: Option<ColorDescription>,
}

fn rav1e_config(p: &Av1EncodeConfig) -> Config {
    // AV1 needs all the CPU power you can give it,
    // except when it'd create inefficiently tiny tiles
    let tiles = {
        let threads = p.threads.unwrap_or_else(rayon::current_num_threads);
        threads.min((p.width * p.height) / (p.speed.min_tile_size as usize).pow(2))
    };
    let speed_settings = p.speed.speed_settings();
    let cfg = Config::new()
        .with_encoder_config(EncoderConfig {
        width: p.width,
        height: p.height,
        time_base: Rational::new(1, 1),
        sample_aspect_ratio: Rational::new(1, 1),
        bit_depth: p.bit_depth,
        chroma_sampling: p.chroma_sampling,
        chroma_sample_position: ChromaSamplePosition::Unknown,
        pixel_range: p.pixel_range,
        color_description: p.color_description,
        mastering_display: None,
        content_light: None,
        enable_timing_info: false,
        still_picture: true,
        error_resilient: false,
        switch_frame_interval: 0,
        min_key_frame_interval: 0,
        max_key_frame_interval: 0,
        reservoir_frame_delay: None,
        low_latency: false,
        quantizer: p.quantizer,
        min_quantizer: p.quantizer as _,
        bitrate: 0,
        tune: Tune::Psychovisual,
        tile_cols: 0,
        tile_rows: 0,
        tiles,
        film_grain_params: None,
        level_idx: None,
        speed_settings,
    });

    if let Some(threads) = p.threads {
        cfg.with_threads(threads)
    } else {
        cfg
    }
}

fn init_frame_3<P: rav1e::Pixel + Default>(
    width: usize, height: usize, planes: impl IntoIterator<Item = [P; 3]> + Send, frame: &mut Frame<P>,
) -> Result<(), Error> {
    let mut f = frame.planes.iter_mut();
    let mut planes = planes.into_iter();

    // it doesn't seem to be necessary to fill padding area
    let mut y = f.next().unwrap().mut_slice(Default::default());
    let mut u = f.next().unwrap().mut_slice(Default::default());
    let mut v = f.next().unwrap().mut_slice(Default::default());

    for ((y, u), v) in y.rows_iter_mut().zip(u.rows_iter_mut()).zip(v.rows_iter_mut()).take(height) {
        let y = &mut y[..width];
        let u = &mut u[..width];
        let v = &mut v[..width];
        for ((y, u), v) in y.iter_mut().zip(u).zip(v) {
            let px = planes.next().ok_or(Error::TooFewPixels)?;
            *y = px[0];
            *u = px[1];
            *v = px[2];
        }
    }
    Ok(())
}

fn init_frame_1<P: rav1e::Pixel + Default>(width: usize, height: usize, planes: impl IntoIterator<Item = P> + Send, frame: &mut Frame<P>) -> Result<(), Error> {
    let mut y = frame.planes[0].mut_slice(Default::default());
    let mut planes = planes.into_iter();

    for y in y.rows_iter_mut().take(height) {
        let y = &mut y[..width];
        for y in y.iter_mut() {
            *y = planes.next().ok_or(Error::TooFewPixels)?;
        }
    }
    Ok(())
}

#[inline(never)]
fn encode_to_av1<P: rav1e::Pixel>(p: &Av1EncodeConfig, init: impl FnOnce(&mut Frame<P>) -> Result<(), Error>) -> Result<Vec<u8>, Error> {
    let mut ctx: Context<P> = rav1e_config(p).new_context()?;
    let mut frame = ctx.new_frame();

    init(&mut frame)?;
    ctx.send_frame(frame)?;
    ctx.flush();

    let mut out = Vec::new();
    loop {
        match ctx.receive_packet() {
            Ok(mut packet) => match packet.frame_type {
                FrameType::KEY => {
                    out.append(&mut packet.data);
                },
                _ => continue,
            },
            Err(EncoderStatus::Encoded | EncoderStatus::LimitReached) => break,
            Err(err) => Err(err)?,
        }
    }
    Ok(out)
}


================================================
FILE: ravif/src/dirtyalpha.rs
================================================
use imgref::{Img, ImgRef};
use rgb::{ComponentMap, RGB, RGBA8};

#[inline]
fn weighed_pixel(px: RGBA8) -> (u16, RGB<u32>) {
    if px.a == 0 {
        return (0, RGB::new(0, 0, 0));
    }
    let weight = 256 - u16::from(px.a);
    (weight, RGB::new(
        u32::from(px.r) * u32::from(weight),
        u32::from(px.g) * u32::from(weight),
        u32::from(px.b) * u32::from(weight)))
}

/// Clear/change RGB components of fully-transparent RGBA pixels to make them cheaper to encode with AV1
pub(crate) fn blurred_dirty_alpha(img: ImgRef<RGBA8>) -> Option<Img<Vec<RGBA8>>> {
    // get dominant visible transparent color (excluding opaque pixels)
    let mut sum = RGB::new(0, 0, 0);
    let mut weights = 0;

    // Only consider colors around transparent images
    // (e.g. solid semitransparent area doesn't need to contribute)
    loop9::loop9_img(img, |_, _, top, mid, bot| {
        if mid.curr.a == 255 || mid.curr.a == 0 {
            return;
        }
        if chain(&top, &mid, &bot).any(|px| px.a == 0) {
            let (w, px) = weighed_pixel(mid.curr);
            weights += u64::from(w);
            sum += px.map(u64::from);
        }
    });
    if weights == 0 {
        return None; // opaque image
    }

    let neutral_alpha = RGBA8::new((sum.r / weights) as u8, (sum.g / weights) as u8, (sum.b / weights) as u8, 0);
    let img2 = bleed_opaque_color(img, neutral_alpha);
    Some(blur_transparent_pixels(img2.as_ref()))
}

/// copy color from opaque pixels to transparent pixels
/// (so that when edges get crushed by compression, the distortion will be away from visible edge)
fn bleed_opaque_color(img: ImgRef<RGBA8>, bg: RGBA8) -> Img<Vec<RGBA8>> {
    let mut out = Vec::with_capacity(img.width() * img.height());
    loop9::loop9_img(img, |_, _, top, mid, bot| {
        out.push(if mid.curr.a == 255 {
            mid.curr
        } else {
            let (weights, sum) = chain(&top, &mid, &bot)
                .map(|c| weighed_pixel(*c))
                .fold((0u32, RGB::new(0, 0, 0)), |mut sum, item| {
                    sum.0 += u32::from(item.0);
                    sum.1 += item.1;
                    sum
                });
            if weights == 0 {
                bg
            } else {
                let mut avg = sum.map(|c| (c / weights) as u8);
                if mid.curr.a == 0 {
                    avg.with_alpha(0)
                } else {
                    // also change non-transparent colors, but only within range where
                    // rounding caused by premultiplied alpha would land on the same color
                    avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a));
                    avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a));
                    avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a));
                    avg.with_alpha(mid.curr.a)
                }
            }
        });
    });
    Img::new(out, img.width(), img.height())
}

/// ensure there are no sharp edges created by the cleared alpha
fn blur_transparent_pixels(img: ImgRef<RGBA8>) -> Img<Vec<RGBA8>> {
    let mut out = Vec::with_capacity(img.width() * img.height());
    loop9::loop9_img(img, |_, _, top, mid, bot| {
        out.push(if mid.curr.a == 255 {
            mid.curr
        } else {
            let sum: RGB<u16> = chain(&top, &mid, &bot).map(|px| px.rgb().map(u16::from)).sum();
            let mut avg = sum.map(|c| (c / 9) as u8);
            if mid.curr.a == 0 {
                avg.with_alpha(0)
            } else {
                // also change non-transparent colors, but only within range where
                // rounding caused by premultiplied alpha would land on the same color
                avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a));
                avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a));
                avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a));
                avg.with_alpha(mid.curr.a)
            }
        });
    });
    Img::new(out, img.width(), img.height())
}

#[inline(always)]
fn chain<'a, T>(top: &'a loop9::Triple<T>, mid: &'a loop9::Triple<T>, bot: &'a loop9::Triple<T>) -> impl Iterator<Item = &'a T> + 'a {
    top.iter().chain(mid.iter()).chain(bot.iter())
}

#[inline]
fn clamp(px: u8, (min, max): (u8, u8)) -> u8 {
    px.max(min).min(max)
}

/// safe range to change px color given its alpha
/// (mostly-transparent colors tolerate more variation)
#[inline]
fn premultiplied_minmax(px: u8, alpha: u8) -> (u8, u8) {
    let alpha = u16::from(alpha);
    let rounded = u16::from(px) * alpha / 255 * 255;

    // leave some spare room for rounding
    let low = ((rounded + 16) / alpha) as u8;
    let hi = ((rounded + 239) / alpha) as u8;

    (low.min(px), hi.max(px))
}

#[test]
fn preminmax() {
    assert_eq!((100, 100), premultiplied_minmax(100, 255));
    assert_eq!((78, 100), premultiplied_minmax(100, 10));
    assert_eq!(100 * 10 / 255, 78 * 10 / 255);
    assert_eq!(100 * 10 / 255, 100 * 10 / 255);
    assert_eq!((8, 119), premultiplied_minmax(100, 2));
    assert_eq!((16, 239), premultiplied_minmax(100, 1));
    assert_eq!((15, 255), premultiplied_minmax(255, 1));
}


================================================
FILE: ravif/src/error.rs
================================================
use quick_error::quick_error;

#[derive(Debug)]
#[doc(hidden)]
pub struct EncodingErrorDetail; // maybe later

quick_error! {
    /// Failures enum
    #[derive(Debug)]
    #[non_exhaustive]
    pub enum Error {
        /// Slices given to `encode_raw_planes` must be `width * height` large.
        TooFewPixels {
            display("Provided buffer is smaller than width * height")
        }
        Unsupported(msg: &'static str) {
            display("Not supported: {}", msg)
        }
        EncodingError(e: EncodingErrorDetail) {
            display("Encoding error reported by rav1e")
            from(_e: rav1e::InvalidConfig) -> (EncodingErrorDetail)
            from(_e: rav1e::EncoderStatus) -> (EncodingErrorDetail)
        }
    }
}


================================================
FILE: ravif/src/lib.rs
================================================
//! ```rust
//! use ravif::*;
//! # fn doit(pixels: &[RGBA8], width: usize, height: usize) -> Result<(), Error> {
//! let res = Encoder::new()
//!     .with_quality(70.)
//!     .with_speed(4)
//!     .encode_rgba(Img::new(pixels, width, height))?;
//! std::fs::write("hello.avif", res.avif_file);
//! # Ok(()) }

mod av1encoder;

mod error;
pub use av1encoder::ColorModel;
pub use error::Error;

#[doc(hidden)]
#[deprecated = "Renamed to `ColorModel`"]
pub type ColorSpace = ColorModel;

pub use av1encoder::{AlphaColorMode, BitDepth, EncodedImage, Encoder};
#[doc(inline)]
pub use rav1e::prelude::{MatrixCoefficients, PixelRange};

mod dirtyalpha;

#[doc(no_inline)]
pub use imgref::Img;
#[doc(no_inline)]
pub use rgb::{RGB8, RGBA8};

#[cfg(not(feature = "threading"))]
mod rayoff {
    pub fn current_num_threads() -> usize {
        std::thread::available_parallelism().map(|v| v.get()).unwrap_or(1)
    }

    pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
        (a(), b())
    }
}

#[test]
fn encode8_with_alpha() {
    let img = imgref::ImgVec::new((0..200).flat_map(|y| (0..256).map(move |x| {
        RGBA8::new(x as u8, y as u8, 255, (x + y) as u8)
    })).collect(), 256, 200);

    let enc = Encoder::new()
        .with_quality(22.0)
        .with_bit_depth(BitDepth::Eight)
        .with_speed(1)
        .with_alpha_quality(22.0)
        .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty)
        .with_num_threads(Some(2));
    let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgba(img.as_ref()).unwrap();
    assert!(color_byte_size > 50 && color_byte_size < 1000);
    assert!(alpha_byte_size > 50 && alpha_byte_size < 1000); // the image must have alpha

    let parsed = avif_parse::read_avif(&mut avif_file.as_slice()).unwrap();
    assert!(parsed.alpha_item.is_some());
    assert!(parsed.primary_item.len() > 100);
    assert!(parsed.primary_item.len() < 1000);

    let md = parsed.primary_item_metadata().unwrap();
    assert_eq!(md.max_frame_width.get(), 256);
    assert_eq!(md.max_frame_height.get(), 200);
    assert_eq!(md.bit_depth, 8);
}

#[test]
fn encode8_opaque() {
    let img = imgref::ImgVec::new((0..101).flat_map(|y| (0..129).map(move |x| {
        RGBA8::new(255, 100 + x as u8, y as u8, 255)
    })).collect(), 129, 101);

    let enc = Encoder::new()
        .with_quality(33.0)
        .with_speed(10)
        .with_alpha_quality(33.0)
        .with_bit_depth(BitDepth::Auto)
        .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty)
        .with_num_threads(Some(1));
    let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgba(img.as_ref()).unwrap();
    assert_eq!(0, alpha_byte_size); // the image must not have alpha
    let tmp_path = format!("/tmp/ravif-encode-test-failure-{color_byte_size}.avif");
    if color_byte_size <= 150 || color_byte_size >= 500 {
        std::fs::write(&tmp_path, &avif_file).expect(&tmp_path);
    }
    assert!(color_byte_size > 150 && color_byte_size < 500, "size = {color_byte_size}; expected ~= 215; see {tmp_path}");

    let parsed1 = avif_parse::read_avif(&mut avif_file.as_slice()).unwrap();
    assert_eq!(None, parsed1.alpha_item);

    let md = parsed1.primary_item_metadata().unwrap();
    assert_eq!(md.max_frame_width.get(), 129);
    assert_eq!(md.max_frame_height.get(), 101);
    assert!(md.still_picture);
    assert_eq!(md.bit_depth, 10);

    let img = img.map_buf(|b| b.into_iter().map(|px| px.rgb()).collect::<Vec<_>>());

    let enc = Encoder::new()
        .with_quality(33.0)
        .with_speed(10)
        .with_bit_depth(BitDepth::Ten)
        .with_alpha_quality(33.0)
        .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty)
        .with_num_threads(Some(1));

    let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgb(img.as_ref()).unwrap();
    assert_eq!(0, alpha_byte_size); // the image must not have alpha
    assert!(color_byte_size > 50 && color_byte_size < 1000);

    let parsed2 = avif_parse::read_avif(&mut avif_file.as_slice()).unwrap();

    assert_eq!(parsed1.alpha_item, parsed2.alpha_item);
    assert_eq!(parsed1.primary_item, parsed2.primary_item); // both are the same pixels
}

#[test]
fn encode8_cleans_alpha() {
    let img = imgref::ImgVec::new((0..200).flat_map(|y| (0..256).map(move |x| {
        RGBA8::new((((x/ 5 + y ) & 0xF) << 4) as u8, (7 * x + y / 2) as u8, ((x * y) & 0x3) as u8, ((x + y) as u8 & 0x7F).saturating_sub(100))
    })).collect(), 256, 200);

    let enc = Encoder::new()
        .with_quality(66.0)
        .with_speed(6)
        .with_alpha_quality(88.0)
        .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty)
        .with_num_threads(Some(1));

    let dirty = enc
        .encode_rgba(img.as_ref())
        .unwrap();

    let clean = enc
        .with_alpha_color_mode(AlphaColorMode::UnassociatedClean)
        .encode_rgba(img.as_ref())
        .unwrap();

    assert_eq!(clean.alpha_byte_size, dirty.alpha_byte_size); // same alpha on both
    assert!(clean.alpha_byte_size > 200 && clean.alpha_byte_size < 1000);
    assert!(clean.color_byte_size > 2000 && clean.color_byte_size < 6000);
    assert!(clean.color_byte_size < dirty.color_byte_size / 2); // significant reduction in color data
}


================================================
FILE: src/main.rs
================================================
use clap::{value_parser, Arg, ArgAction, Command};
use imgref::ImgVec;
use ravif::{AlphaColorMode, BitDepth, ColorModel, EncodedImage, Encoder, RGBA8};
use rayon::prelude::*;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};

type BoxError = Box<dyn std::error::Error + Send + Sync>;

fn main() {
    if let Err(e) = run() {
        eprintln!("error: {e}");
        let mut source = e.source();
        while let Some(e) = source {
            eprintln!("  because: {e}");
            source = e.source();
        }
        std::process::exit(1);
    }
}

enum MaybePath {
    Stdio,
    Path(PathBuf),
}

fn parse_quality(arg: &str) -> Result<f32, String> {
    let q = arg.parse::<f32>().map_err(|e| e.to_string())?;
    if q < 1. || q > 100. {
        return Err("quality must be in 1-100 range".into());
    }
    Ok(q)
}

fn parse_speed(arg: &str) -> Result<u8, String> {
    let s = arg.parse::<u8>().map_err(|e| e.to_string())?;
    if s < 1 || s > 100 {
        return Err("speed must be in 1-10 range".into());
    }
    Ok(s)
}

fn run() -> Result<(), BoxError> {
    let args = Command::new("cavif-rs")
        .version(clap::crate_version!())
        .author("Kornel Lesiński <kornel@imageoptim.com>")
        .about("Convert JPEG/PNG images to AVIF image format (based on AV1/rav1e)")
        .arg(Arg::new("quality")
            .short('Q')
            .long("quality")
            .value_name("n")
            .value_parser(parse_quality)
            .default_value("80")
            .help("Quality from 1 (worst) to 100 (best)"))
        .arg(Arg::new("speed")
            .short('s')
            .long("speed")
            .value_name("n")
            .default_value("4")
            .value_parser(parse_speed)
            .help("Encoding speed from 1 (best) to 10 (fast but ugly)"))
        .arg(Arg::new("threads")
            .short('j')
            .long("threads")
            .value_name("n")
            .default_value("0")
            .value_parser(value_parser!(u8))
            .help("Maximum threads to use (0 = one thread per host core)"))
        .arg(Arg::new("overwrite")
            .alias("force")
            .short('f')
            .long("overwrite")
            .action(ArgAction::SetTrue)
            .num_args(0)
            .help("Replace files if there's .avif already"))
        .arg(Arg::new("output")
            .short('o')
            .long("output")
            .value_parser(value_parser!(PathBuf))
            .value_name("path")
            .help("Write output to this path instead of same_file.avif. It may be a file or a directory."))
        .arg(Arg::new("quiet")
            .short('q')
            .long("quiet")
            .action(ArgAction::SetTrue)
            .num_args(0)
            .help("Don't print anything"))
        .arg(Arg::new("dirty-alpha")
            .long("dirty-alpha")
            .action(ArgAction::SetTrue)
            .num_args(0)
            .help("Keep RGB data of fully-transparent pixels (makes larger, lower quality files)"))
        .arg(Arg::new("color")
            .long("color")
            .default_value("ycbcr")
            .value_parser(["ycbcr", "rgb"])
            .help("Internal AVIF color model. YCbCr works better for human eyes."))
        .arg(Arg::new("depth")
            .long("depth")
            .default_value("auto")
            .value_parser(["8", "10", "auto"])
            .help("Write 8-bit (more compatible) or 10-bit (better quality) images"))
        .arg(Arg::new("IMAGES")
            .index(1)
            .num_args(1..)
            .value_parser(value_parser!(PathBuf))
            .help("One or more JPEG or PNG files to convert. \"-\" is interpreted as stdin/stdout."))
        .get_matches();

    let output = args.get_one::<PathBuf>("output").map(|s| match s {
        s if s.as_os_str() == "-" => MaybePath::Stdio,
        s => MaybePath::Path(PathBuf::from(s)),
    });
    let quality = *args.get_one::<f32>("quality").expect("default");
    let alpha_quality = ((quality + 100.) / 2.).min(quality + quality / 4. + 2.);
    let speed: u8 = *args.get_one::<u8>("speed").expect("default");
    let overwrite = args.get_flag("overwrite");
    let quiet = args.get_flag("quiet");
    let threads = args.get_one::<u8>("threads").copied();
    let dirty_alpha = args.get_flag("dirty-alpha");

    let color_model = match args.get_one::<String>("color").expect("default").as_str() {
        "ycbcr" => ColorModel::YCbCr,
        "rgb" => ColorModel::RGB,
        x => Err(format!("bad color type: {x}"))?,
    };

    let depth = match args.get_one::<String>("depth").expect("default").as_str() {
        "8" => BitDepth::Eight,
        "10" => BitDepth::Ten,
        _ => BitDepth::Auto,
    };

    let files = args.get_many::<PathBuf>("IMAGES").ok_or("Please specify image paths to convert")?;
    let files: Vec<_> = files
        .filter(|pathstr| {
            let path = Path::new(&pathstr);
            if let Some(s) = path.to_str() {
                if quiet && s.parse::<u8>().is_ok() && !path.exists() {
                    eprintln!("warning: -q is not for quality, so '{s}' is misinterpreted as a file. Use -Q {s}");
                }
            }
            path.extension().is_none_or(|e| if e == "avif" {
                if !quiet {
                    if path.exists() {
                        eprintln!("warning: ignoring {}, because it's already an AVIF", path.display());
                    } else {
                        eprintln!("warning: Did you mean to use -o {p}?", p = path.display());
                        return true;
                    }
                }
                false
            } else {
                true
            })
        })
        .map(|p| if p.as_os_str() == "-" {
            MaybePath::Stdio
        } else {
            MaybePath::Path(PathBuf::from(p))
        })
        .collect();

    if files.is_empty() {
        return Err("No PNG/JPEG files specified".into());
    }

    let use_dir = match output {
        Some(MaybePath::Path(ref path)) => {
            if files.len() > 1 {
                let _ = fs::create_dir_all(path);
            }
            files.len() > 1 || path.is_dir()
        },
        _ => false,
    };

    let process = move |data: Vec<u8>, input_path: &MaybePath| -> Result<(), BoxError> {
        let img = load_rgba(&data, false)?;
        drop(data);
        let out_path = match (&output, input_path) {
            (None, MaybePath::Path(input)) => MaybePath::Path(input.with_extension("avif")),
            (Some(MaybePath::Path(output)), MaybePath::Path(input)) => MaybePath::Path({
                if use_dir {
                    output.join(Path::new(input.file_name().unwrap()).with_extension("avif"))
                } else {
                    output.clone()
                }
            }),
            (None, MaybePath::Stdio) |
            (Some(MaybePath::Stdio), _) => MaybePath::Stdio,
            (Some(MaybePath::Path(output)), MaybePath::Stdio) => MaybePath::Path(output.clone()),
        };
        match out_path {
            MaybePath::Path(ref p) if !overwrite && p.exists() => {
                return Err(format!("{} already exists; skipping", p.display()).into());
            },
            _ => {},
        }
        let enc = Encoder::new()
            .with_quality(quality)
            .with_bit_depth(depth)
            .with_speed(speed)
            .with_alpha_quality(alpha_quality)
            .with_internal_color_model(color_model)
            .with_alpha_color_mode(if dirty_alpha { AlphaColorMode::UnassociatedDirty } else { AlphaColorMode::UnassociatedClean })
            .with_num_threads(threads.filter(|&n| n > 0).map(usize::from));
        let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgba(img.as_ref())?;
        match out_path {
            MaybePath::Path(ref p) => {
                if !quiet {
                    println!("{}: {}KB ({color_byte_size}B color, {alpha_byte_size}B alpha, {}B HEIF)", p.display(), avif_file.len().div_ceil(1000), avif_file.len() - color_byte_size - alpha_byte_size);
                }
                fs::write(p, avif_file)
            },
            MaybePath::Stdio => std::io::stdout().write_all(&avif_file),
        }
        .map_err(|e| format!("Unable to write output image: {e}"))?;
        Ok(())
    };

    let failures = files.into_par_iter().map(|path| {
            let tmp;
            let (data, path_str): (_, &dyn std::fmt::Display) = match path {
                MaybePath::Stdio => {
                    let mut data = Vec::new();
                    std::io::stdin().read_to_end(&mut data)?;
                    (data, &"stdin")
                },
                MaybePath::Path(ref path) => {
                    let data = fs::read(path).map_err(|e| format!("Unable to read input image {}: {e}", path.display()))?;
                    tmp = path.display();
                    (data, &tmp)
                },
            };
            process(data, &path)
                .map_err(|e| BoxError::from(format!("{path_str}: error: {e}")))
        })
        .filter_map(|res| res.err())
        .collect::<Vec<BoxError>>();

    if !failures.is_empty() {
        if !quiet {
            for f in failures {
                eprintln!("error: {f}");
            }
        }
        std::process::exit(1);
    }
    Ok(())
}

#[cfg(not(feature = "cocoa_image"))]
fn load_rgba(data: &[u8], premultiplied_alpha: bool) -> Result<ImgVec<RGBA8>, BoxError> {
    use rgb::prelude::*;

    let img = load_image::load_data(data)?.into_imgvec();
    let mut img = match img {
        load_image::export::imgref::ImgVecKind::RGB8(img) => img.map_buf(|buf| buf.into_iter().map(|px| px.with_alpha(255)).collect()),
        load_image::export::imgref::ImgVecKind::RGBA8(img) => img,
        load_image::export::imgref::ImgVecKind::RGB16(img) => img.map_buf(|buf| buf.into_iter().map(|px| px.map(|c| (c >> 8) as u8).with_alpha(255)).collect()),
        load_image::export::imgref::ImgVecKind::RGBA16(img) => img.map_buf(|buf| buf.into_iter().map(|px| px.map(|c| (c >> 8) as u8)).collect()),
        load_image::export::imgref::ImgVecKind::GRAY8(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = g.value(); RGBA8::new(c,c,c,255) }).collect()),
        load_image::export::imgref::ImgVecKind::GRAY16(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = (g.value()>>8) as u8; RGBA8::new(c,c,c,255) }).collect()),
        load_image::export::imgref::ImgVecKind::GRAYA8(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = g.value(); RGBA8::new(c,c,c,g.a) }).collect()),
        load_image::export::imgref::ImgVecKind::GRAYA16(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = (g.value()>>8) as u8; RGBA8::new(c,c,c,(g.a>>8) as u8) }).collect()),
    };

    if premultiplied_alpha {
        img.pixels_mut().for_each(|px| {
            px.r = (u16::from(px.r) * u16::from(px.a) / 255) as u8;
            px.g = (u16::from(px.g) * u16::from(px.a) / 255) as u8;
            px.b = (u16::from(px.b) * u16::from(px.a) / 255) as u8;
        });
    }
    Ok(img)
}

#[cfg(feature = "cocoa_image")]
fn load_rgba(data: &[u8], premultiplied_alpha: bool) -> Result<ImgVec<RGBA8>, BoxError> {
    if premultiplied_alpha {
        Ok(cocoa_image::decode_image_as_rgba_premultiplied(data)?)
    } else {
        Ok(cocoa_image::decode_image_as_rgba(data)?)
    }
}


================================================
FILE: tests/stdio.rs
================================================
use std::io::{Read, Write};
use std::process::Stdio;

#[test]
fn stdio() -> Result<(), std::io::Error> {
    let img = include_bytes!("testimage.png");

    let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_cavif"))
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .arg("-")
        .arg("--speed=10")
        .spawn()?;

    let mut stdin = cmd.stdin.take().unwrap();
    let _ = std::thread::spawn(move || {
        stdin.write_all(img).unwrap();
    });

    let mut data = Vec::new();
    cmd.stdout.take().unwrap().read_to_end(&mut data)?;
    assert!(cmd.wait()?.success());
    assert_eq!(&data[4..4 + 8], b"ftypavif");
    Ok(())
}

#[test]
fn path_to_stdout() -> Result<(), std::io::Error> {
    let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_cavif"))
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .arg("tests/testimage.png")
        .arg("--speed=10")
        .arg("-o")
        .arg("-")
        .spawn()?;

    let mut data = Vec::new();
    cmd.stdout.take().unwrap().read_to_end(&mut data)?;
    assert!(cmd.wait()?.success());
    avif_parse::read_avif(&mut data.as_slice()).unwrap();
    Ok(())
}
Download .txt
gitextract_35ej9mke/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .rustfmt.toml
├── Cargo.toml
├── LICENSE
├── README.md
├── ravif/
│   ├── Cargo.toml
│   ├── README.md
│   └── src/
│       ├── av1encoder.rs
│       ├── dirtyalpha.rs
│       ├── error.rs
│       └── lib.rs
├── src/
│   └── main.rs
└── tests/
    └── stdio.rs
Download .txt
SYMBOL INDEX (66 symbols across 6 files)

FILE: ravif/src/av1encoder.rs
  type ColorModel (line 13) | pub enum ColorModel {
  type AlphaColorMode (line 26) | pub enum AlphaColorMode {
  type BitDepth (line 43) | pub enum BitDepth {
  type EncodedImage (line 54) | pub struct EncodedImage {
  type Encoder (line 67) | pub struct Encoder<'exif_slice> {
  method default (line 89) | fn default() -> Self {
  function new (line 108) | pub fn new() -> Self {
  function with_quality (line 116) | pub fn with_quality(mut self, quality: f32) -> Self {
  function with_depth (line 125) | pub fn with_depth(self, depth: Option<u8>) -> Self {
  function with_bit_depth (line 136) | pub fn with_bit_depth(mut self, depth: BitDepth) -> Self {
  function with_alpha_quality (line 145) | pub fn with_alpha_quality(mut self, quality: f32) -> Self {
  function with_speed (line 158) | pub fn with_speed(mut self, speed: u8) -> Self {
  function with_internal_color_model (line 170) | pub fn with_internal_color_model(mut self, color_model: ColorModel) -> S...
  function with_internal_color_space (line 178) | pub fn with_internal_color_space(self, color_model: ColorModel) -> Self {
  function with_num_threads (line 187) | pub fn with_num_threads(mut self, num_threads: Option<usize>) -> Self {
  function with_alpha_color_mode (line 199) | pub fn with_alpha_color_mode(mut self, mode: AlphaColorMode) -> Self {
  function with_exif (line 208) | pub fn with_exif(mut self, exif_data: impl Into<Cow<'exif_slice, [u8]>>)...
  function set_exif (line 216) | pub fn set_exif(&mut self, exif_data: impl Into<Cow<'exif_slice, [u8]>>) {
  function encode_rgba (line 243) | pub fn encode_rgba(&self, in_buffer: Img<&[rgb::RGBA<u8>]>) -> Result<En...
  function convert_alpha_8bit (line 277) | fn convert_alpha_8bit(&self, in_buffer: Img<&[RGBA8]>) -> Option<ImgVec<...
  function encode_rgb (line 318) | pub fn encode_rgb(&self, buffer: Img<&[RGB8]>) -> Result<EncodedImage, E...
  function encode_rgb_internal_from_8bit (line 322) | fn encode_rgb_internal_from_8bit(&self, width: usize, height: usize, pix...
  function encode_raw_planes_8_bit (line 366) | pub fn encode_raw_planes_8_bit(
  function encode_raw_planes_10_bit (line 390) | pub fn encode_raw_planes_10_bit(
  function encode_raw_planes_internal (line 400) | fn encode_raw_planes_internal<P: rav1e::Pixel + Default>(
  function to_ten (line 485) | fn to_ten(x: u8) -> u16 {
  function rgb_to_10_bit_gbr (line 491) | fn rgb_to_10_bit_gbr(px: rgb::RGB<u8>) -> (u16, u16, u16) {
  function rgb_to_8_bit_gbr (line 496) | fn rgb_to_8_bit_gbr(px: rgb::RGB<u8>) -> (u8, u8, u8) {
  constant BT601 (line 501) | const BT601: [f32; 3] = [0.2990, 0.5870, 0.1140];
  function rgb_to_ycbcr (line 504) | fn rgb_to_ycbcr(px: rgb::RGB<u8>, depth: u8, matrix: [f32; 3]) -> (f32, ...
  function rgb_to_10_bit_ycbcr (line 515) | fn rgb_to_10_bit_ycbcr(px: rgb::RGB<u8>, matrix: [f32; 3]) -> (u16, u16,...
  function rgb_to_8_bit_ycbcr (line 521) | fn rgb_to_8_bit_ycbcr(px: rgb::RGB<u8>, matrix: [f32; 3]) -> (u8, u8, u8) {
  function quality_to_quantizer (line 526) | fn quality_to_quantizer(quality: f32) -> u8 {
  type SpeedTweaks (line 533) | struct SpeedTweaks {
    method from_my_preset (line 555) | pub fn from_my_preset(speed: u8, quantizer: u8) -> Self {
    method speed_settings (line 608) | pub(crate) fn speed_settings(&self) -> SpeedSettings {
  type Av1EncodeConfig (line 649) | struct Av1EncodeConfig {
  function rav1e_config (line 662) | fn rav1e_config(p: &Av1EncodeConfig) -> Config {
  function init_frame_3 (line 710) | fn init_frame_3<P: rav1e::Pixel + Default>(
  function init_frame_1 (line 735) | fn init_frame_1<P: rav1e::Pixel + Default>(width: usize, height: usize, ...
  function encode_to_av1 (line 749) | fn encode_to_av1<P: rav1e::Pixel>(p: &Av1EncodeConfig, init: impl FnOnce...

FILE: ravif/src/dirtyalpha.rs
  function weighed_pixel (line 5) | fn weighed_pixel(px: RGBA8) -> (u16, RGB<u32>) {
  function blurred_dirty_alpha (line 17) | pub(crate) fn blurred_dirty_alpha(img: ImgRef<RGBA8>) -> Option<Img<Vec<...
  function bleed_opaque_color (line 45) | fn bleed_opaque_color(img: ImgRef<RGBA8>, bg: RGBA8) -> Img<Vec<RGBA8>> {
  function blur_transparent_pixels (line 79) | fn blur_transparent_pixels(img: ImgRef<RGBA8>) -> Img<Vec<RGBA8>> {
  function chain (line 103) | fn chain<'a, T>(top: &'a loop9::Triple<T>, mid: &'a loop9::Triple<T>, bo...
  function clamp (line 108) | fn clamp(px: u8, (min, max): (u8, u8)) -> u8 {
  function premultiplied_minmax (line 115) | fn premultiplied_minmax(px: u8, alpha: u8) -> (u8, u8) {
  function preminmax (line 127) | fn preminmax() {

FILE: ravif/src/error.rs
  type EncodingErrorDetail (line 5) | pub struct EncodingErrorDetail;

FILE: ravif/src/lib.rs
  type ColorSpace (line 19) | pub type ColorSpace = ColorModel;
  function current_num_threads (line 34) | pub fn current_num_threads() -> usize {
  function join (line 38) | pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {
  function encode8_with_alpha (line 44) | fn encode8_with_alpha() {
  function encode8_opaque (line 72) | fn encode8_opaque() {
  function encode8_cleans_alpha (line 122) | fn encode8_cleans_alpha() {

FILE: src/main.rs
  type BoxError (line 9) | type BoxError = Box<dyn std::error::Error + Send + Sync>;
  function main (line 11) | fn main() {
  type MaybePath (line 23) | enum MaybePath {
  function parse_quality (line 28) | fn parse_quality(arg: &str) -> Result<f32, String> {
  function parse_speed (line 36) | fn parse_speed(arg: &str) -> Result<u8, String> {
  function run (line 44) | fn run() -> Result<(), BoxError> {
  function load_rgba (line 255) | fn load_rgba(data: &[u8], premultiplied_alpha: bool) -> Result<ImgVec<RG...
  function load_rgba (line 281) | fn load_rgba(data: &[u8], premultiplied_alpha: bool) -> Result<ImgVec<RG...

FILE: tests/stdio.rs
  function stdio (line 5) | fn stdio() -> Result<(), std::io::Error> {
  function path_to_stdout (line 28) | fn path_to_stdout() -> Result<(), std::io::Error> {
Condensed preview — 14 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (67K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 381,
    "preview": "name: Rust\n\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@"
  },
  {
    "path": ".gitignore",
    "chars": 19,
    "preview": "target/\nCargo.lock\n"
  },
  {
    "path": ".rustfmt.toml",
    "chars": 690,
    "preview": "array_width = 120\nbinop_separator = \"Back\"\nchain_width = 120\ncomment_width = 222\ncondense_wildcard_suffixes = true\ndisab"
  },
  {
    "path": "Cargo.toml",
    "chars": 1480,
    "preview": "[package]\nname = \"cavif\"\ndescription = \"Encodes images in AVIF format (image2avif converter) using a pure-Rust encoder.\""
  },
  {
    "path": "LICENSE",
    "chars": 1514,
    "preview": "BSD 3-Clause License\n\nCopyright (c) 2020, Kornel\nAll rights reserved.\n\nRedistribution and use in source and binary forms"
  },
  {
    "path": "README.md",
    "chars": 3194,
    "preview": "# `cavif` — PNG/JPEG to AVIF converter\n\nEncoder/converter for AVIF images. Based on [`rav1e`](https://lib.rs/crates/rav1"
  },
  {
    "path": "ravif/Cargo.toml",
    "chars": 1229,
    "preview": "[package]\nname = \"ravif\"\ndescription = \"rav1e-based pure Rust library for encoding images in AVIF format (powers the `ca"
  },
  {
    "path": "ravif/README.md",
    "chars": 472,
    "preview": "# `ravif` — Pure Rust library for AVIF image encoding\n\nEncoder for AVIF images. Based on [`rav1e`](https://lib.rs/crates"
  },
  {
    "path": "ravif/src/av1encoder.rs",
    "chars": 31061,
    "preview": "#![allow(deprecated)]\nuse std::borrow::Cow;\nuse crate::dirtyalpha::blurred_dirty_alpha;\nuse crate::error::Error;\n#[cfg(n"
  },
  {
    "path": "ravif/src/dirtyalpha.rs",
    "chars": 5240,
    "preview": "use imgref::{Img, ImgRef};\nuse rgb::{ComponentMap, RGB, RGBA8};\n\n#[inline]\nfn weighed_pixel(px: RGBA8) -> (u16, RGB<u32>"
  },
  {
    "path": "ravif/src/error.rs",
    "chars": 750,
    "preview": "use quick_error::quick_error;\n\n#[derive(Debug)]\n#[doc(hidden)]\npub struct EncodingErrorDetail; // maybe later\n\nquick_err"
  },
  {
    "path": "ravif/src/lib.rs",
    "chars": 5327,
    "preview": "//! ```rust\n//! use ravif::*;\n//! # fn doit(pixels: &[RGBA8], width: usize, height: usize) -> Result<(), Error> {\n//! le"
  },
  {
    "path": "src/main.rs",
    "chars": 11461,
    "preview": "use clap::{value_parser, Arg, ArgAction, Command};\nuse imgref::ImgVec;\nuse ravif::{AlphaColorMode, BitDepth, ColorModel,"
  },
  {
    "path": "tests/stdio.rs",
    "chars": 1184,
    "preview": "use std::io::{Read, Write};\nuse std::process::Stdio;\n\n#[test]\nfn stdio() -> Result<(), std::io::Error> {\n    let img = i"
  }
]

About this extraction

This page contains the full source code of the kornelski/cavif-rs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 14 files (62.5 KB), approximately 17.2k tokens, and a symbol index with 66 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!