[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Rust\n\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - uses: dtolnay/rust-toolchain@stable\n    - uses: ilammy/setup-nasm@v1\n    - name: Tests\n      run: cargo test --verbose --all --all-targets\n    - name: Check semver\n      uses: obi1kenobi/cargo-semver-checks-action@v2\n      with:\n        package: ravif\n"
  },
  {
    "path": ".gitignore",
    "content": "target/\nCargo.lock\n"
  },
  {
    "path": ".rustfmt.toml",
    "content": "array_width = 120\nbinop_separator = \"Back\"\nchain_width = 120\ncomment_width = 222\ncondense_wildcard_suffixes = true\ndisable_all_formatting = true\nedition = \"2024\"\nenum_discrim_align_threshold = 5\nfn_call_width = 120\nfn_params_layout = \"Compressed\"\nfn_single_line = false\nforce_explicit_abi = false\nformat_code_in_doc_comments = true\nimports_granularity = \"Module\"\nimports_layout = \"Horizontal\"\nmatch_block_trailing_comma = true\nmax_width = 160\noverflow_delimited_expr = true\nreorder_impl_items = true\nsingle_line_if_else_max_width = 150\nstruct_lit_width = 40\nuse_field_init_shorthand = true\nuse_small_heuristics = \"Max\"\nuse_try_shorthand = true\nwhere_single_line = true\nwrap_comments = true\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"cavif\"\ndescription = \"Encodes images in AVIF format (image2avif converter) using a pure-Rust encoder.\"\nversion = \"1.7.0\"\nauthors = [\"Kornel Lesiński <kornel@geekhood.net>\"]\nedition = \"2024\"\nlicense = \"BSD-3-Clause\"\nreadme = \"README.md\"\nkeywords = [\"avif\", \"png2avif\", \"jpeg2avif\", \"convert\", \"av1\"]\ncategories = [\"command-line-utilities\", \"multimedia::images\", \"multimedia::encoding\"]\nhomepage = \"https://lib.rs/crates/cavif\"\nrepository = \"https://github.com/kornelski/cavif-rs\"\ninclude = [\"README.md\", \"LICENSE\", \"/src/*.rs\"]\nrust-version = \"1.85\"\n\n[dependencies]\nclap = { version = \"4.5.40\", default-features = false, features = [\"color\", \"suggestions\", \"wrap_help\", \"std\", \"cargo\"] }\ncocoa_image = { version = \"1.1.0\", optional = true }\nimgref = \"1.11.0\"\nload_image = \"3.2.1\"\nravif = { version = \"0.13\", path = \"./ravif\", default-features = false, features = [\"threading\"] }\nrayon = \"1.11.0\"\nrgb = { version = \"0.8.52\", default-features = false }\n\n[features]\ndefault = [\"asm\", \"static\"]\nasm = [\"ravif/asm\"]\nstatic = [\"load_image/lcms2-static\"]\n\n[profile.dev]\nopt-level = 1\ndebug = 1\n\n[profile.release]\nopt-level = 3\npanic = \"abort\"\ndebug = false\nlto = true\nstrip = true\n\n[profile.dev.package.\"*\"]\nopt-level = 2\n\n[dev-dependencies]\navif-parse = \"1.4.0\"\n\n[badges]\nmaintenance = { status = \"actively-developed\" }\n\n[workspace]\nmembers = [\"ravif\"]\n\n[package.metadata.docs.rs]\ntargets = [\"x86_64-unknown-linux-gnu\"]\nrustdoc-args = [\"--generate-link-to-definition\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2020, Kornel\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# `cavif` — PNG/JPEG to AVIF converter\n\nEncoder/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).\n\n## Installation\n\n➡️ **[Download the latest release](https://github.com/kornelski/cavif/releases)** ⬅️\n\nThe 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.\n\n## Usage\n\nRun in a terminal (hint: you don't need to type the path, terminals accept file drag'n'drop)\n\n```bash\ncavif image.png\n```\n\nIt makes `image.avif`. You can adjust quality (it's in 1-100 scale):\n\n```bash\ncavif --quality 60 image.png\n```\n\n### Advanced usage\n\nYou can also specify multiple images. Encoding is multi-threaded, so the more, the better!\n\n```text\ncavif [OPTIONS] IMAGES...\n```\n\n * `--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.\n * `--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.\n * `--overwrite` — Replace files if there's `.avif` already. By default the existing files are left untouched.\n * `-o path` — Write images to this path (instead of `same-name.avif`). If multiple input files are specified, it's interpreted as a directory.\n * `--quiet` — Don't print anything during conversion.\n\nThere 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:\n\n * `--dirty-alpha` — Preserve RGB values of fully transparent pixels (not recommended). By default irrelevant color of transparent pixels is cleared to avoid wasting space.\n * `--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.\n * `--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.\n\n## Compatibility\n\nImages [work in all modern browsers](https://caniuse.com/avif).\n\n* Chrome 85+ desktop,\n* Chrome on Android 12,\n* Firefox 91,\n* Safari iOS 16/macOS Ventura.\n\n### Known incompatibilities\n\n* 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.\n* Windows' preview doesn't seem to support 10-bit deep images. Use `--depth=8` when encoding if this is a problem.\n\n## Building\n\nTo build it from source you need Rust 1.67 or later, preferably via [rustup](https://rustup.rs).\n\nThen run in a terminal:\n\n```bash\nrustup update\ncargo install cavif\n```\n"
  },
  {
    "path": "ravif/Cargo.toml",
    "content": "[package]\nname = \"ravif\"\ndescription = \"rav1e-based pure Rust library for encoding images in AVIF format (powers the `cavif` tool)\"\nversion = \"0.13.0\"\nauthors = [\"Kornel Lesiński <kornel@geekhood.net>\"]\nedition = \"2024\"\nlicense = \"BSD-3-Clause\"\nreadme = \"README.md\"\nkeywords = [\"avif\", \"convert\", \"av1\", \"rav1f\", \"cav1f\"]\ncategories = [\"multimedia::images\", \"multimedia::encoding\"]\nhomepage = \"https://lib.rs/crates/ravif\"\nrepository = \"https://github.com/kornelski/cavif-rs\"\ninclude = [\"README.md\", \"LICENSE\", \"Cargo.toml\", \"/src/*.rs\"]\nrust-version = \"1.85\"\n\n[dependencies]\navif-serialize = \"0.8.6\"\nimgref = \"1.12.0\"\nrav1e = { version = \"0.8.1\", default-features = false }\nrayon = { version = \"1.11.0\", optional = true }\nrgb = { version = \"0.8.52\", default-features = false }\nloop9 = \"0.1.5\"\nquick-error = \"2.0.1\"\n\n[target.'cfg(target = \"wasm32-unknown-unknown\")'.dependencies]\nrav1e = { version = \"0.8\", default-features = false, features = [\"wasm\"] }\n\n[features]\ndefault = [\"asm\", \"threading\"]\nasm = [\"rav1e/asm\"]\nthreading = [\"dep:rayon\", \"rav1e/threading\"]\n\n[profile.release]\nlto = true\n\n[profile.dev.package.\"*\"]\ndebug = false\nopt-level = 2\n\n[dev-dependencies]\navif-parse = \"1.4.0\"\n\n[package.metadata.release]\ntag = false\n"
  },
  {
    "path": "ravif/README.md",
    "content": "# `ravif` — Pure Rust library for AVIF image encoding\n\nEncoder for AVIF images. Based on [`rav1e`](https://lib.rs/crates/rav1e) and [`avif-serialize`](https://lib.rs/crates/avif-serialize).\n\nThe API is just a single `encode_rgba()` function call that spits an AVIF image.\n\nThis 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`.\n"
  },
  {
    "path": "ravif/src/av1encoder.rs",
    "content": "#![allow(deprecated)]\nuse std::borrow::Cow;\nuse crate::dirtyalpha::blurred_dirty_alpha;\nuse crate::error::Error;\n#[cfg(not(feature = \"threading\"))]\nuse crate::rayoff as rayon;\nuse imgref::{Img, ImgVec};\nuse rav1e::prelude::*;\nuse rgb::{RGB8, RGBA8};\n\n/// For [`Encoder::with_internal_color_model`]\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum ColorModel {\n    /// Standard color model for photographic content. Usually the best choice.\n    /// This library always uses full-resolution color (4:4:4).\n    /// This library will automatically choose between BT.601 or BT.709.\n    YCbCr,\n    /// RGB channels are encoded without color space transformation.\n    /// Usually results in larger file sizes, and is less compatible than `YCbCr`.\n    /// Use only if the content really makes use of RGB, e.g. anaglyph images or RGB subpixel anti-aliasing.\n    RGB,\n}\n\n/// Handling of color channels in transparent images. For [`Encoder::with_alpha_color_mode`]\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum AlphaColorMode {\n    /// Use unassociated alpha channel and leave color channels unchanged, even if there's redundant color data in transparent areas.\n    UnassociatedDirty,\n    /// Use unassociated alpha channel, but set color channels of transparent areas to a solid color to eliminate invisible data and improve compression.\n    UnassociatedClean,\n    /// Store color channels of transparent images in premultiplied form.\n    /// This requires support for premultiplied alpha in AVIF decoders.\n    ///\n    /// It may reduce file sizes due to clearing of fully-transparent pixels, but\n    /// may also increase file sizes due to creation of new edges in the color channels.\n    ///\n    /// Note that this is only internal detail for the AVIF file.\n    /// It does not change meaning of `RGBA` in this library — it's always unassociated.\n    Premultiplied,\n}\n\n#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]\npub enum BitDepth {\n    Eight,\n    Ten,\n    /// Same as `Ten`\n    #[default]\n    Auto,\n}\n\n/// The newly-created image file + extra info FYI\n#[non_exhaustive]\n#[derive(Clone)]\npub struct EncodedImage {\n    /// AVIF (HEIF+AV1) encoded image data\n    pub avif_file: Vec<u8>,\n    /// FYI: number of bytes of AV1 payload used for the color\n    pub color_byte_size: usize,\n    /// FYI: number of bytes of AV1 payload used for the alpha channel\n    pub alpha_byte_size: usize,\n}\n\n/// Encoder config builder\n///\n/// The lifetime is relevant only for [`Encoder::with_exif()`]. Use `Encoder<'static>` if Rust complains.\n#[derive(Debug, Clone)]\npub struct Encoder<'exif_slice> {\n    /// 0-255 scale\n    quantizer: u8,\n    /// 0-255 scale\n    alpha_quantizer: u8,\n    /// rav1e preset 1 (slow) 10 (fast but crappy)\n    speed: u8,\n    /// True if RGBA input has already been premultiplied. It inserts appropriate metadata.\n    premultiplied_alpha: bool,\n    /// Which pixel format to use in AVIF file. RGB tends to give larger files.\n    color_model: ColorModel,\n    /// How many threads should be used (0 = match core count), None - use global rayon thread pool\n    threads: Option<usize>,\n    /// [`AlphaColorMode`]\n    alpha_color_mode: AlphaColorMode,\n    /// 8 or 10\n    output_depth: BitDepth,\n    /// Dropped into MPEG infe BOX\n    exif: Option<Cow<'exif_slice, [u8]>>,\n}\n\nimpl<'exif_slice> Default for Encoder<'exif_slice> {\n    fn default() -> Self {\n        Self {\n           quantizer: quality_to_quantizer(80.),\n           alpha_quantizer: quality_to_quantizer(80.),\n           speed: 5,\n           output_depth: BitDepth::default(),\n           premultiplied_alpha: false,\n           color_model: ColorModel::YCbCr,\n           threads: None,\n           exif: None,\n           alpha_color_mode: AlphaColorMode::UnassociatedClean,\n       }\n    }\n}\n\n/// Builder methods\nimpl<'exif_slice> Encoder<'exif_slice> {\n    /// Start here\n    #[must_use]\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    /// Quality `1..=100`. Panics if out of range.\n    #[inline(always)]\n    #[track_caller]\n    #[must_use]\n    pub fn with_quality(mut self, quality: f32) -> Self {\n        assert!(quality >= 1. && quality <= 100.);\n        self.quantizer = quality_to_quantizer(quality);\n        self\n    }\n\n    #[doc(hidden)]\n    #[deprecated(note = \"Renamed to with_bit_depth\")]\n    #[must_use]\n    pub fn with_depth(self, depth: Option<u8>) -> Self {\n        self.with_bit_depth(depth.map(|d| if d >= 10 { BitDepth::Ten } else { BitDepth::Eight }).unwrap_or(BitDepth::Auto))\n    }\n\n    /// 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.\n    ///\n    /// Use 8-bit depth only as a workaround for decoders that need it.\n    ///\n    /// This setting does not affect pixel inputs for this library.\n    #[inline(always)]\n    #[must_use]\n    pub fn with_bit_depth(mut self, depth: BitDepth) -> Self {\n        self.output_depth = depth;\n        self\n    }\n\n    /// Quality for the alpha channel only. `1..=100`. Panics if out of range.\n    #[inline(always)]\n    #[track_caller]\n    #[must_use]\n    pub fn with_alpha_quality(mut self, quality: f32) -> Self {\n        assert!(quality >= 1. && quality <= 100.);\n        self.alpha_quantizer = quality_to_quantizer(quality);\n        self\n    }\n\n    /// * 1 = very very slow, but max compression.\n    /// * 10 = quick, but larger file sizes and lower quality.\n    ///\n    /// Panics if outside `1..=10`.\n    #[inline(always)]\n    #[track_caller]\n    #[must_use]\n    pub fn with_speed(mut self, speed: u8) -> Self {\n        assert!(speed >= 1 && speed <= 10);\n        self.speed = speed;\n        self\n    }\n\n    /// Changes how color channels are stored in the image. The default is YCbCr.\n    ///\n    /// Note that this is only internal detail for the AVIF file, and doesn't\n    /// change color model of inputs to encode functions.\n    #[inline(always)]\n    #[must_use]\n    pub fn with_internal_color_model(mut self, color_model: ColorModel) -> Self {\n        self.color_model = color_model;\n        self\n    }\n\n    #[doc(hidden)]\n    #[deprecated = \"Renamed to `with_internal_color_model()`\"]\n    #[must_use]\n    pub fn with_internal_color_space(self, color_model: ColorModel) -> Self {\n        self.with_internal_color_model(color_model)\n    }\n\n    /// Configures `rayon` thread pool size.\n    /// The default `None` is to use all threads in the default `rayon` thread pool.\n    #[inline(always)]\n    #[track_caller]\n    #[must_use]\n    pub fn with_num_threads(mut self, num_threads: Option<usize>) -> Self {\n        assert!(num_threads.is_none_or(|n| n > 0));\n        self.threads = num_threads;\n        self\n    }\n\n    /// Configure handling of color channels in transparent images\n    ///\n    /// Note that this doesn't affect input format for this library,\n    /// which for RGBA is always uncorrelated alpha.\n    #[inline(always)]\n    #[must_use]\n    pub fn with_alpha_color_mode(mut self, mode: AlphaColorMode) -> Self {\n        self.alpha_color_mode = mode;\n        self.premultiplied_alpha = mode == AlphaColorMode::Premultiplied;\n        self\n    }\n\n    /// Embedded into AVIF file as-is\n    ///\n    /// The data can be `Vec<u8>`, or `&[u8]` if the encoder instance doesn't leave its scope.\n    pub fn with_exif(mut self, exif_data: impl Into<Cow<'exif_slice, [u8]>>) -> Self {\n        self.set_exif(exif_data);\n        self\n    }\n\n    /// Embedded into AVIF file as-is\n    ///\n    /// The data can be `Vec<u8>`, or `&[u8]` if the encoder instance doesn't leave its scope.\n    pub fn set_exif(&mut self, exif_data: impl Into<Cow<'exif_slice, [u8]>>) {\n        self.exif = Some(exif_data.into());\n    }\n}\n\n/// Once done with config, call one of the `encode_*` functions\nimpl Encoder<'_> {\n    /// Make a new AVIF image from RGBA pixels (non-premultiplied, alpha last)\n    ///\n    /// Make the `Img` for the `buffer` like this:\n    ///\n    /// ```rust,ignore\n    /// Img::new(&pixels_rgba[..], width, height)\n    /// ```\n    ///\n    /// If you have pixels as `u8` slice, then use the `rgb` crate, and do:\n    ///\n    /// ```rust,ignore\n    /// use rgb::ComponentSlice;\n    /// let pixels_rgba = pixels_u8.as_rgba();\n    /// ```\n    ///\n    /// If all pixels are opaque, the alpha channel will be left out automatically.\n    ///\n    /// This function takes 8-bit inputs, but will generate an AVIF file using 10-bit depth.\n    ///\n    /// returns AVIF file with info about sizes about AV1 payload.\n    pub fn encode_rgba(&self, in_buffer: Img<&[rgb::RGBA<u8>]>) -> Result<EncodedImage, Error> {\n        let new_alpha = self.convert_alpha_8bit(in_buffer);\n        let buffer = new_alpha.as_ref().map(|b| b.as_ref()).unwrap_or(in_buffer);\n        let use_alpha = buffer.pixels().any(|px| px.a != 255);\n        if !use_alpha {\n            return self.encode_rgb_internal_from_8bit(buffer.width(), buffer.height(), buffer.pixels().map(|px| px.rgb()));\n        }\n\n        let width = buffer.width();\n        let height = buffer.height();\n        let matrix_coefficients = match self.color_model {\n            ColorModel::YCbCr => MatrixCoefficients::BT601,\n            ColorModel::RGB => MatrixCoefficients::Identity,\n        };\n        match self.output_depth {\n            BitDepth::Eight => {\n                let planes = buffer.pixels().map(|px| match self.color_model {\n                    ColorModel::YCbCr => rgb_to_8_bit_ycbcr(px.rgb(), BT601).into(),\n                    ColorModel::RGB => rgb_to_8_bit_gbr(px.rgb()).into(),\n                });\n                let alpha = buffer.pixels().map(|px| px.a);\n                self.encode_raw_planes_8_bit(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients)\n            },\n            BitDepth::Ten | BitDepth::Auto => {\n                let planes = buffer.pixels().map(|px| match self.color_model {\n                    ColorModel::YCbCr => rgb_to_10_bit_ycbcr(px.rgb(), BT601).into(),\n                    ColorModel::RGB => rgb_to_10_bit_gbr(px.rgb()).into(),\n                });\n                let alpha = buffer.pixels().map(|px| to_ten(px.a));\n                self.encode_raw_planes_10_bit(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients)\n            },\n        }\n    }\n\n    fn convert_alpha_8bit(&self, in_buffer: Img<&[RGBA8]>) -> Option<ImgVec<RGBA8>> {\n        match self.alpha_color_mode {\n            AlphaColorMode::UnassociatedDirty => None,\n            AlphaColorMode::UnassociatedClean => blurred_dirty_alpha(in_buffer),\n            AlphaColorMode::Premultiplied => {\n                let prem = in_buffer.pixels()\n                    .map(|px| {\n                        if px.a == 0 || px.a == 255 {\n                            RGBA8::default()\n                        } else {\n                            RGBA8::new(\n                                (u16::from(px.r) * 255 / u16::from(px.a)) as u8,\n                                (u16::from(px.g) * 255 / u16::from(px.a)) as u8,\n                                (u16::from(px.b) * 255 / u16::from(px.a)) as u8,\n                                px.a,\n                            )\n                        }\n                    })\n                    .collect();\n                Some(ImgVec::new(prem, in_buffer.width(), in_buffer.height()))\n            },\n        }\n    }\n\n    /// Make a new AVIF image from RGB pixels\n    ///\n    /// Make the `Img` for the `buffer` like this:\n    ///\n    /// ```rust,ignore\n    /// Img::new(&pixels_rgb[..], width, height)\n    /// ```\n    ///\n    /// If you have pixels as `u8` slice, then first do:\n    ///\n    /// ```rust,ignore\n    /// use rgb::ComponentSlice;\n    /// let pixels_rgb = pixels_u8.as_rgb();\n    /// ```\n    ///\n    /// returns AVIF file, size of color metadata\n    #[inline]\n    pub fn encode_rgb(&self, buffer: Img<&[RGB8]>) -> Result<EncodedImage, Error> {\n        self.encode_rgb_internal_from_8bit(buffer.width(), buffer.height(), buffer.pixels())\n    }\n\n    fn encode_rgb_internal_from_8bit(&self, width: usize, height: usize, pixels: impl Iterator<Item = RGB8> + Send + Sync) -> Result<EncodedImage, Error> {\n        let matrix_coefficients = match self.color_model {\n            ColorModel::YCbCr => MatrixCoefficients::BT601,\n            ColorModel::RGB => MatrixCoefficients::Identity,\n        };\n\n        match self.output_depth {\n            BitDepth::Eight => {\n                let planes = pixels.map(|px| {\n                    let (y, u, v) = match self.color_model {\n                        ColorModel::YCbCr => rgb_to_8_bit_ycbcr(px, BT601),\n                        ColorModel::RGB => rgb_to_8_bit_gbr(px),\n                    };\n                    [y, u, v]\n                });\n                self.encode_raw_planes_8_bit(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients)\n            },\n            BitDepth::Ten | BitDepth::Auto => {\n                let planes = pixels.map(|px| {\n                    let (y, u, v) = match self.color_model {\n                        ColorModel::YCbCr => rgb_to_10_bit_ycbcr(px, BT601),\n                        ColorModel::RGB => rgb_to_10_bit_gbr(px),\n                    };\n                    [y, u, v]\n                });\n                self.encode_raw_planes_10_bit(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients)\n            },\n        }\n    }\n\n    /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`,\n    /// with sRGB transfer characteristics and color primaries.\n    ///\n    /// Alpha always uses full range. Chroma subsampling is not supported, and it's a bad idea for AVIF anyway.\n    /// If there's no alpha, use `None::<[_; 0]>`.\n    ///\n    /// `color_pixel_range` should be `PixelRange::Full` to avoid worsening already small 8-bit dynamic range.\n    /// Support for limited range may be removed in the future.\n    ///\n    /// If `AlphaColorMode::Premultiplied` has been set, the alpha pixels must be premultiplied.\n    /// `AlphaColorMode::UnassociatedClean` has no effect in this function, and is equivalent to `AlphaColorMode::UnassociatedDirty`.\n    ///\n    /// returns AVIF file, size of color metadata, size of alpha metadata overhead\n    #[inline]\n    pub fn encode_raw_planes_8_bit(\n        &self, width: usize, height: usize,\n        planes: impl IntoIterator<Item = [u8; 3]> + Send,\n        alpha: Option<impl IntoIterator<Item = u8> + Send>,\n        color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients,\n    ) -> Result<EncodedImage, Error> {\n        self.encode_raw_planes_internal(width, height, planes, alpha, color_pixel_range, matrix_coefficients, 8)\n    }\n\n    /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`,\n    /// with sRGB transfer characteristics and color primaries.\n    ///\n    /// The pixels are 10-bit (values `0.=1023`) in host's native endian.\n    ///\n    /// Alpha always uses full range. Chroma subsampling is not supported, and it's a bad idea for AVIF anyway.\n    /// If there's no alpha, use `None::<[_; 0]>`.\n    ///\n    /// `color_pixel_range` should be `PixelRange::Full`. Support for limited range may be removed in the future.\n    ///\n    /// If `AlphaColorMode::Premultiplied` has been set, the alpha pixels must be premultiplied.\n    /// `AlphaColorMode::UnassociatedClean` has no effect in this function, and is equivalent to `AlphaColorMode::UnassociatedDirty`.\n    ///\n    /// returns AVIF file, size of color metadata, size of alpha metadata overhead\n    #[inline]\n    pub fn encode_raw_planes_10_bit(\n        &self, width: usize, height: usize,\n        planes: impl IntoIterator<Item = [u16; 3]> + Send,\n        alpha: Option<impl IntoIterator<Item = u16> + Send>,\n        color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients,\n    ) -> Result<EncodedImage, Error> {\n        self.encode_raw_planes_internal(width, height, planes, alpha, color_pixel_range, matrix_coefficients, 10)\n    }\n\n    #[inline(never)]\n    fn encode_raw_planes_internal<P: rav1e::Pixel + Default>(\n        &self, width: usize, height: usize,\n        planes: impl IntoIterator<Item = [P; 3]> + Send,\n        alpha: Option<impl IntoIterator<Item = P> + Send>,\n        color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients,\n        input_pixels_bit_depth: u8,\n    ) -> Result<EncodedImage, Error> {\n        let color_description = Some(ColorDescription {\n            transfer_characteristics: TransferCharacteristics::SRGB,\n            color_primaries: ColorPrimaries::BT709, // sRGB-compatible\n            matrix_coefficients,\n        });\n\n        let threads = self.threads.map(|threads| {\n            if threads > 0 { threads } else { rayon::current_num_threads() }\n        });\n\n        let encode_color = move || {\n            encode_to_av1::<P>(\n                &Av1EncodeConfig {\n                    width,\n                    height,\n                    bit_depth: input_pixels_bit_depth.into(),\n                    quantizer: self.quantizer.into(),\n                    speed: SpeedTweaks::from_my_preset(self.speed, self.quantizer),\n                    threads,\n                    pixel_range: color_pixel_range,\n                    chroma_sampling: ChromaSampling::Cs444,\n                    color_description,\n                },\n                move |frame| init_frame_3(width, height, planes, frame),\n            )\n        };\n        let encode_alpha = move || {\n            alpha.map(|alpha| {\n                encode_to_av1::<P>(\n                    &Av1EncodeConfig {\n                        width,\n                        height,\n                        bit_depth: input_pixels_bit_depth.into(),\n                        quantizer: self.alpha_quantizer.into(),\n                        speed: SpeedTweaks::from_my_preset(self.speed, self.alpha_quantizer),\n                        threads,\n                        pixel_range: PixelRange::Full,\n                        chroma_sampling: ChromaSampling::Cs400,\n                        color_description: None,\n                    },\n                    |frame| init_frame_1(width, height, alpha, frame),\n                )\n            })\n        };\n        #[cfg(all(target_arch = \"wasm32\", not(target_feature = \"atomics\")))]\n        let (color, alpha) = (encode_color(), encode_alpha());\n        #[cfg(not(all(target_arch = \"wasm32\", not(target_feature = \"atomics\"))))]\n        let (color, alpha) = rayon::join(encode_color, encode_alpha);\n        let (color, alpha) = (color?, alpha.transpose()?);\n\n        let mut serializer_config = avif_serialize::Aviffy::new();\n        serializer_config\n            .matrix_coefficients(match matrix_coefficients {\n                MatrixCoefficients::Identity => avif_serialize::constants::MatrixCoefficients::Rgb,\n                MatrixCoefficients::BT709 => avif_serialize::constants::MatrixCoefficients::Bt709,\n                MatrixCoefficients::Unspecified => avif_serialize::constants::MatrixCoefficients::Unspecified,\n                MatrixCoefficients::BT601 => avif_serialize::constants::MatrixCoefficients::Bt601,\n                MatrixCoefficients::YCgCo => avif_serialize::constants::MatrixCoefficients::Ycgco,\n                MatrixCoefficients::BT2020NCL => avif_serialize::constants::MatrixCoefficients::Bt2020Ncl,\n                MatrixCoefficients::BT2020CL => avif_serialize::constants::MatrixCoefficients::Bt2020Cl,\n                _ => return Err(Error::Unsupported(\"matrix coefficients\")),\n            })\n            .premultiplied_alpha(self.premultiplied_alpha);\n        if let Some(exif) = &self.exif {\n            serializer_config.set_exif(exif.to_vec());\n        }\n        let avif_file = serializer_config.to_vec(&color, alpha.as_deref(), width as u32, height as u32, input_pixels_bit_depth);\n        let color_byte_size = color.len();\n        let alpha_byte_size = alpha.as_ref().map_or(0, |a| a.len());\n\n        Ok(EncodedImage {\n            avif_file, color_byte_size, alpha_byte_size,\n        })\n    }\n}\n\n/// Native endian\n#[inline(always)]\nfn to_ten(x: u8) -> u16 {\n    (u16::from(x) << 2) | (u16::from(x) >> 6)\n}\n\n/// Native endian\n#[inline(always)]\nfn rgb_to_10_bit_gbr(px: rgb::RGB<u8>) -> (u16, u16, u16) {\n    (to_ten(px.g), to_ten(px.b), to_ten(px.r))\n}\n\n#[inline(always)]\nfn rgb_to_8_bit_gbr(px: rgb::RGB<u8>) -> (u8, u8, u8) {\n    (px.g, px.b, px.r)\n}\n\n// const REC709: [f32; 3] = [0.2126, 0.7152, 0.0722];\nconst BT601: [f32; 3] = [0.2990, 0.5870, 0.1140];\n\n#[inline(always)]\nfn rgb_to_ycbcr(px: rgb::RGB<u8>, depth: u8, matrix: [f32; 3]) -> (f32, f32, f32) {\n    let max_value = ((1 << depth) - 1) as f32;\n    let scale = max_value / 255.;\n    let shift = (max_value * 0.5).round();\n    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)));\n    let cb = f32::from(px.b).mul_add(scale, -y).mul_add(0.5 / (1. - matrix[2]), shift);\n    let cr = f32::from(px.r).mul_add(scale, -y).mul_add(0.5 / (1. - matrix[0]), shift);\n    (y.round(), cb.round(), cr.round())\n}\n\n#[inline(always)]\nfn rgb_to_10_bit_ycbcr(px: rgb::RGB<u8>, matrix: [f32; 3]) -> (u16, u16, u16) {\n    let (y, u, v) = rgb_to_ycbcr(px, 10, matrix);\n    (y as u16, u as u16, v as u16)\n}\n\n#[inline(always)]\nfn rgb_to_8_bit_ycbcr(px: rgb::RGB<u8>, matrix: [f32; 3]) -> (u8, u8, u8) {\n    let (y, u, v) = rgb_to_ycbcr(px, 8, matrix);\n    (y as u8, u as u8, v as u8)\n}\n\nfn quality_to_quantizer(quality: f32) -> u8 {\n    let q = quality / 100.;\n    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 };\n    (x * 255.).round() as u8\n}\n\n#[derive(Debug, Copy, Clone)]\nstruct SpeedTweaks {\n    pub speed_preset: u8,\n\n    pub fast_deblock: Option<bool>,\n    pub reduced_tx_set: Option<bool>,\n    pub tx_domain_distortion: Option<bool>,\n    pub tx_domain_rate: Option<bool>,\n    pub encode_bottomup: Option<bool>,\n    pub rdo_tx_decision: Option<bool>,\n    pub cdef: Option<bool>,\n    /// loop restoration filter\n    pub lrf: Option<bool>,\n    pub sgr_complexity_full: Option<bool>,\n    pub use_satd_subpel: Option<bool>,\n    pub inter_tx_split: Option<bool>,\n    pub fine_directional_intra: Option<bool>,\n    pub complex_prediction_modes: Option<bool>,\n    pub partition_range: Option<(u8, u8)>,\n    pub min_tile_size: u16,\n}\n\nimpl SpeedTweaks {\n    pub fn from_my_preset(speed: u8, quantizer: u8) -> Self {\n        let low_quality = quantizer < quality_to_quantizer(55.);\n        let high_quality = quantizer > quality_to_quantizer(80.);\n        let max_block_size = if high_quality { 16 } else { 64 };\n\n        Self {\n            speed_preset: speed,\n\n            partition_range: Some(match speed {\n                0 => (4, 64.min(max_block_size)),\n                1 if low_quality => (4, 64.min(max_block_size)),\n                2 if low_quality => (4, 32.min(max_block_size)),\n                1..=4 => (4, 16),\n                5..=8 => (8, 16),\n                _ => (16, 16),\n            }),\n\n            complex_prediction_modes: Some(speed <= 1), // 2x-3x slower, 2% better\n            sgr_complexity_full: Some(speed <= 2), // 15% slower, barely improves anything -/+1%\n\n            encode_bottomup: Some(speed <= 2), // may be costly (+60%), may even backfire\n\n            // big blocks disabled at 3\n\n            // these two are together?\n            rdo_tx_decision: Some(speed <= 4 && !high_quality), // it tends to blur subtle textures\n            reduced_tx_set: Some(speed == 4 || speed >= 9), // It interacts with tx_domain_distortion too?\n\n            // 4px blocks disabled at 5\n\n            fine_directional_intra: Some(speed <= 6),\n            fast_deblock: Some(speed >= 7 && !high_quality), // mixed bag?\n\n            // 8px blocks disabled at 8\n            lrf: Some(low_quality && speed <= 8), // hardly any help for hi-q images. recovers some q at low quality\n            cdef: Some(low_quality && speed <= 9), // hardly any help for hi-q images. recovers some q at low quality\n\n            inter_tx_split: Some(speed >= 9), // mixed bag even when it works, and it backfires if not used together with reduced_tx_set\n            tx_domain_rate: Some(speed >= 10), // 20% faster, but also 10% larger files!\n\n            tx_domain_distortion: None, // very mixed bag, sometimes helps speed sometimes it doesn't\n            use_satd_subpel: Some(false), // doesn't make sense\n            min_tile_size: match speed {\n                0 => 4096,\n                1 => 2048,\n                2 => 1024,\n                3 => 512,\n                4 => 256,\n                _ => 128,\n            } * if high_quality { 2 } else { 1 },\n        }\n    }\n\n    pub(crate) fn speed_settings(&self) -> SpeedSettings {\n        let mut speed_settings = SpeedSettings::from_preset(self.speed_preset);\n\n        speed_settings.multiref = false;\n        speed_settings.rdo_lookahead_frames = 1;\n        speed_settings.scene_detection_mode = SceneDetectionSpeed::None;\n        speed_settings.motion.include_near_mvs = false;\n\n        if let Some(v) = self.fast_deblock { speed_settings.fast_deblock = v; }\n        if let Some(v) = self.reduced_tx_set { speed_settings.transform.reduced_tx_set = v; }\n        if let Some(v) = self.tx_domain_distortion { speed_settings.transform.tx_domain_distortion = v; }\n        if let Some(v) = self.tx_domain_rate { speed_settings.transform.tx_domain_rate = v; }\n        if let Some(v) = self.encode_bottomup { speed_settings.partition.encode_bottomup = v; }\n        if let Some(v) = self.rdo_tx_decision { speed_settings.transform.rdo_tx_decision = v; }\n        if let Some(v) = self.cdef { speed_settings.cdef = v; }\n        if let Some(v) = self.lrf { speed_settings.lrf = v; }\n        if let Some(v) = self.inter_tx_split { speed_settings.transform.enable_inter_tx_split = v; }\n        if let Some(v) = self.sgr_complexity_full { speed_settings.sgr_complexity = if v { SGRComplexityLevel::Full } else { SGRComplexityLevel::Reduced } }\n        if let Some(v) = self.use_satd_subpel { speed_settings.motion.use_satd_subpel = v; }\n        if let Some(v) = self.fine_directional_intra { speed_settings.prediction.fine_directional_intra = v; }\n        if let Some(v) = self.complex_prediction_modes { speed_settings.prediction.prediction_modes = if v { PredictionModesSetting::ComplexAll } else { PredictionModesSetting::Simple} }\n        if let Some((min, max)) = self.partition_range {\n            debug_assert!(min <= max);\n            fn sz(s: u8) -> BlockSize {\n                match s {\n                    4 => BlockSize::BLOCK_4X4,\n                    8 => BlockSize::BLOCK_8X8,\n                    16 => BlockSize::BLOCK_16X16,\n                    32 => BlockSize::BLOCK_32X32,\n                    64 => BlockSize::BLOCK_64X64,\n                    128 => BlockSize::BLOCK_128X128,\n                    _ => panic!(\"bad size {s}\"),\n                }\n            }\n            speed_settings.partition.partition_range = PartitionRange::new(sz(min), sz(max));\n        }\n\n        speed_settings\n    }\n}\n\nstruct Av1EncodeConfig {\n    pub width: usize,\n    pub height: usize,\n    pub bit_depth: usize,\n    pub quantizer: usize,\n    pub speed: SpeedTweaks,\n    /// 0 means num_cpus\n    pub threads: Option<usize>,\n    pub pixel_range: PixelRange,\n    pub chroma_sampling: ChromaSampling,\n    pub color_description: Option<ColorDescription>,\n}\n\nfn rav1e_config(p: &Av1EncodeConfig) -> Config {\n    // AV1 needs all the CPU power you can give it,\n    // except when it'd create inefficiently tiny tiles\n    let tiles = {\n        let threads = p.threads.unwrap_or_else(rayon::current_num_threads);\n        threads.min((p.width * p.height) / (p.speed.min_tile_size as usize).pow(2))\n    };\n    let speed_settings = p.speed.speed_settings();\n    let cfg = Config::new()\n        .with_encoder_config(EncoderConfig {\n        width: p.width,\n        height: p.height,\n        time_base: Rational::new(1, 1),\n        sample_aspect_ratio: Rational::new(1, 1),\n        bit_depth: p.bit_depth,\n        chroma_sampling: p.chroma_sampling,\n        chroma_sample_position: ChromaSamplePosition::Unknown,\n        pixel_range: p.pixel_range,\n        color_description: p.color_description,\n        mastering_display: None,\n        content_light: None,\n        enable_timing_info: false,\n        still_picture: true,\n        error_resilient: false,\n        switch_frame_interval: 0,\n        min_key_frame_interval: 0,\n        max_key_frame_interval: 0,\n        reservoir_frame_delay: None,\n        low_latency: false,\n        quantizer: p.quantizer,\n        min_quantizer: p.quantizer as _,\n        bitrate: 0,\n        tune: Tune::Psychovisual,\n        tile_cols: 0,\n        tile_rows: 0,\n        tiles,\n        film_grain_params: None,\n        level_idx: None,\n        speed_settings,\n    });\n\n    if let Some(threads) = p.threads {\n        cfg.with_threads(threads)\n    } else {\n        cfg\n    }\n}\n\nfn init_frame_3<P: rav1e::Pixel + Default>(\n    width: usize, height: usize, planes: impl IntoIterator<Item = [P; 3]> + Send, frame: &mut Frame<P>,\n) -> Result<(), Error> {\n    let mut f = frame.planes.iter_mut();\n    let mut planes = planes.into_iter();\n\n    // it doesn't seem to be necessary to fill padding area\n    let mut y = f.next().unwrap().mut_slice(Default::default());\n    let mut u = f.next().unwrap().mut_slice(Default::default());\n    let mut v = f.next().unwrap().mut_slice(Default::default());\n\n    for ((y, u), v) in y.rows_iter_mut().zip(u.rows_iter_mut()).zip(v.rows_iter_mut()).take(height) {\n        let y = &mut y[..width];\n        let u = &mut u[..width];\n        let v = &mut v[..width];\n        for ((y, u), v) in y.iter_mut().zip(u).zip(v) {\n            let px = planes.next().ok_or(Error::TooFewPixels)?;\n            *y = px[0];\n            *u = px[1];\n            *v = px[2];\n        }\n    }\n    Ok(())\n}\n\nfn init_frame_1<P: rav1e::Pixel + Default>(width: usize, height: usize, planes: impl IntoIterator<Item = P> + Send, frame: &mut Frame<P>) -> Result<(), Error> {\n    let mut y = frame.planes[0].mut_slice(Default::default());\n    let mut planes = planes.into_iter();\n\n    for y in y.rows_iter_mut().take(height) {\n        let y = &mut y[..width];\n        for y in y.iter_mut() {\n            *y = planes.next().ok_or(Error::TooFewPixels)?;\n        }\n    }\n    Ok(())\n}\n\n#[inline(never)]\nfn encode_to_av1<P: rav1e::Pixel>(p: &Av1EncodeConfig, init: impl FnOnce(&mut Frame<P>) -> Result<(), Error>) -> Result<Vec<u8>, Error> {\n    let mut ctx: Context<P> = rav1e_config(p).new_context()?;\n    let mut frame = ctx.new_frame();\n\n    init(&mut frame)?;\n    ctx.send_frame(frame)?;\n    ctx.flush();\n\n    let mut out = Vec::new();\n    loop {\n        match ctx.receive_packet() {\n            Ok(mut packet) => match packet.frame_type {\n                FrameType::KEY => {\n                    out.append(&mut packet.data);\n                },\n                _ => continue,\n            },\n            Err(EncoderStatus::Encoded | EncoderStatus::LimitReached) => break,\n            Err(err) => Err(err)?,\n        }\n    }\n    Ok(out)\n}\n"
  },
  {
    "path": "ravif/src/dirtyalpha.rs",
    "content": "use imgref::{Img, ImgRef};\nuse rgb::{ComponentMap, RGB, RGBA8};\n\n#[inline]\nfn weighed_pixel(px: RGBA8) -> (u16, RGB<u32>) {\n    if px.a == 0 {\n        return (0, RGB::new(0, 0, 0));\n    }\n    let weight = 256 - u16::from(px.a);\n    (weight, RGB::new(\n        u32::from(px.r) * u32::from(weight),\n        u32::from(px.g) * u32::from(weight),\n        u32::from(px.b) * u32::from(weight)))\n}\n\n/// Clear/change RGB components of fully-transparent RGBA pixels to make them cheaper to encode with AV1\npub(crate) fn blurred_dirty_alpha(img: ImgRef<RGBA8>) -> Option<Img<Vec<RGBA8>>> {\n    // get dominant visible transparent color (excluding opaque pixels)\n    let mut sum = RGB::new(0, 0, 0);\n    let mut weights = 0;\n\n    // Only consider colors around transparent images\n    // (e.g. solid semitransparent area doesn't need to contribute)\n    loop9::loop9_img(img, |_, _, top, mid, bot| {\n        if mid.curr.a == 255 || mid.curr.a == 0 {\n            return;\n        }\n        if chain(&top, &mid, &bot).any(|px| px.a == 0) {\n            let (w, px) = weighed_pixel(mid.curr);\n            weights += u64::from(w);\n            sum += px.map(u64::from);\n        }\n    });\n    if weights == 0 {\n        return None; // opaque image\n    }\n\n    let neutral_alpha = RGBA8::new((sum.r / weights) as u8, (sum.g / weights) as u8, (sum.b / weights) as u8, 0);\n    let img2 = bleed_opaque_color(img, neutral_alpha);\n    Some(blur_transparent_pixels(img2.as_ref()))\n}\n\n/// copy color from opaque pixels to transparent pixels\n/// (so that when edges get crushed by compression, the distortion will be away from visible edge)\nfn bleed_opaque_color(img: ImgRef<RGBA8>, bg: RGBA8) -> Img<Vec<RGBA8>> {\n    let mut out = Vec::with_capacity(img.width() * img.height());\n    loop9::loop9_img(img, |_, _, top, mid, bot| {\n        out.push(if mid.curr.a == 255 {\n            mid.curr\n        } else {\n            let (weights, sum) = chain(&top, &mid, &bot)\n                .map(|c| weighed_pixel(*c))\n                .fold((0u32, RGB::new(0, 0, 0)), |mut sum, item| {\n                    sum.0 += u32::from(item.0);\n                    sum.1 += item.1;\n                    sum\n                });\n            if weights == 0 {\n                bg\n            } else {\n                let mut avg = sum.map(|c| (c / weights) as u8);\n                if mid.curr.a == 0 {\n                    avg.with_alpha(0)\n                } else {\n                    // also change non-transparent colors, but only within range where\n                    // rounding caused by premultiplied alpha would land on the same color\n                    avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a));\n                    avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a));\n                    avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a));\n                    avg.with_alpha(mid.curr.a)\n                }\n            }\n        });\n    });\n    Img::new(out, img.width(), img.height())\n}\n\n/// ensure there are no sharp edges created by the cleared alpha\nfn blur_transparent_pixels(img: ImgRef<RGBA8>) -> Img<Vec<RGBA8>> {\n    let mut out = Vec::with_capacity(img.width() * img.height());\n    loop9::loop9_img(img, |_, _, top, mid, bot| {\n        out.push(if mid.curr.a == 255 {\n            mid.curr\n        } else {\n            let sum: RGB<u16> = chain(&top, &mid, &bot).map(|px| px.rgb().map(u16::from)).sum();\n            let mut avg = sum.map(|c| (c / 9) as u8);\n            if mid.curr.a == 0 {\n                avg.with_alpha(0)\n            } else {\n                // also change non-transparent colors, but only within range where\n                // rounding caused by premultiplied alpha would land on the same color\n                avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a));\n                avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a));\n                avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a));\n                avg.with_alpha(mid.curr.a)\n            }\n        });\n    });\n    Img::new(out, img.width(), img.height())\n}\n\n#[inline(always)]\nfn chain<'a, T>(top: &'a loop9::Triple<T>, mid: &'a loop9::Triple<T>, bot: &'a loop9::Triple<T>) -> impl Iterator<Item = &'a T> + 'a {\n    top.iter().chain(mid.iter()).chain(bot.iter())\n}\n\n#[inline]\nfn clamp(px: u8, (min, max): (u8, u8)) -> u8 {\n    px.max(min).min(max)\n}\n\n/// safe range to change px color given its alpha\n/// (mostly-transparent colors tolerate more variation)\n#[inline]\nfn premultiplied_minmax(px: u8, alpha: u8) -> (u8, u8) {\n    let alpha = u16::from(alpha);\n    let rounded = u16::from(px) * alpha / 255 * 255;\n\n    // leave some spare room for rounding\n    let low = ((rounded + 16) / alpha) as u8;\n    let hi = ((rounded + 239) / alpha) as u8;\n\n    (low.min(px), hi.max(px))\n}\n\n#[test]\nfn preminmax() {\n    assert_eq!((100, 100), premultiplied_minmax(100, 255));\n    assert_eq!((78, 100), premultiplied_minmax(100, 10));\n    assert_eq!(100 * 10 / 255, 78 * 10 / 255);\n    assert_eq!(100 * 10 / 255, 100 * 10 / 255);\n    assert_eq!((8, 119), premultiplied_minmax(100, 2));\n    assert_eq!((16, 239), premultiplied_minmax(100, 1));\n    assert_eq!((15, 255), premultiplied_minmax(255, 1));\n}\n"
  },
  {
    "path": "ravif/src/error.rs",
    "content": "use quick_error::quick_error;\n\n#[derive(Debug)]\n#[doc(hidden)]\npub struct EncodingErrorDetail; // maybe later\n\nquick_error! {\n    /// Failures enum\n    #[derive(Debug)]\n    #[non_exhaustive]\n    pub enum Error {\n        /// Slices given to `encode_raw_planes` must be `width * height` large.\n        TooFewPixels {\n            display(\"Provided buffer is smaller than width * height\")\n        }\n        Unsupported(msg: &'static str) {\n            display(\"Not supported: {}\", msg)\n        }\n        EncodingError(e: EncodingErrorDetail) {\n            display(\"Encoding error reported by rav1e\")\n            from(_e: rav1e::InvalidConfig) -> (EncodingErrorDetail)\n            from(_e: rav1e::EncoderStatus) -> (EncodingErrorDetail)\n        }\n    }\n}\n"
  },
  {
    "path": "ravif/src/lib.rs",
    "content": "//! ```rust\n//! use ravif::*;\n//! # fn doit(pixels: &[RGBA8], width: usize, height: usize) -> Result<(), Error> {\n//! let res = Encoder::new()\n//!     .with_quality(70.)\n//!     .with_speed(4)\n//!     .encode_rgba(Img::new(pixels, width, height))?;\n//! std::fs::write(\"hello.avif\", res.avif_file);\n//! # Ok(()) }\n\nmod av1encoder;\n\nmod error;\npub use av1encoder::ColorModel;\npub use error::Error;\n\n#[doc(hidden)]\n#[deprecated = \"Renamed to `ColorModel`\"]\npub type ColorSpace = ColorModel;\n\npub use av1encoder::{AlphaColorMode, BitDepth, EncodedImage, Encoder};\n#[doc(inline)]\npub use rav1e::prelude::{MatrixCoefficients, PixelRange};\n\nmod dirtyalpha;\n\n#[doc(no_inline)]\npub use imgref::Img;\n#[doc(no_inline)]\npub use rgb::{RGB8, RGBA8};\n\n#[cfg(not(feature = \"threading\"))]\nmod rayoff {\n    pub fn current_num_threads() -> usize {\n        std::thread::available_parallelism().map(|v| v.get()).unwrap_or(1)\n    }\n\n    pub fn join<A, B>(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) {\n        (a(), b())\n    }\n}\n\n#[test]\nfn encode8_with_alpha() {\n    let img = imgref::ImgVec::new((0..200).flat_map(|y| (0..256).map(move |x| {\n        RGBA8::new(x as u8, y as u8, 255, (x + y) as u8)\n    })).collect(), 256, 200);\n\n    let enc = Encoder::new()\n        .with_quality(22.0)\n        .with_bit_depth(BitDepth::Eight)\n        .with_speed(1)\n        .with_alpha_quality(22.0)\n        .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty)\n        .with_num_threads(Some(2));\n    let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgba(img.as_ref()).unwrap();\n    assert!(color_byte_size > 50 && color_byte_size < 1000);\n    assert!(alpha_byte_size > 50 && alpha_byte_size < 1000); // the image must have alpha\n\n    let parsed = avif_parse::read_avif(&mut avif_file.as_slice()).unwrap();\n    assert!(parsed.alpha_item.is_some());\n    assert!(parsed.primary_item.len() > 100);\n    assert!(parsed.primary_item.len() < 1000);\n\n    let md = parsed.primary_item_metadata().unwrap();\n    assert_eq!(md.max_frame_width.get(), 256);\n    assert_eq!(md.max_frame_height.get(), 200);\n    assert_eq!(md.bit_depth, 8);\n}\n\n#[test]\nfn encode8_opaque() {\n    let img = imgref::ImgVec::new((0..101).flat_map(|y| (0..129).map(move |x| {\n        RGBA8::new(255, 100 + x as u8, y as u8, 255)\n    })).collect(), 129, 101);\n\n    let enc = Encoder::new()\n        .with_quality(33.0)\n        .with_speed(10)\n        .with_alpha_quality(33.0)\n        .with_bit_depth(BitDepth::Auto)\n        .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty)\n        .with_num_threads(Some(1));\n    let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgba(img.as_ref()).unwrap();\n    assert_eq!(0, alpha_byte_size); // the image must not have alpha\n    let tmp_path = format!(\"/tmp/ravif-encode-test-failure-{color_byte_size}.avif\");\n    if color_byte_size <= 150 || color_byte_size >= 500 {\n        std::fs::write(&tmp_path, &avif_file).expect(&tmp_path);\n    }\n    assert!(color_byte_size > 150 && color_byte_size < 500, \"size = {color_byte_size}; expected ~= 215; see {tmp_path}\");\n\n    let parsed1 = avif_parse::read_avif(&mut avif_file.as_slice()).unwrap();\n    assert_eq!(None, parsed1.alpha_item);\n\n    let md = parsed1.primary_item_metadata().unwrap();\n    assert_eq!(md.max_frame_width.get(), 129);\n    assert_eq!(md.max_frame_height.get(), 101);\n    assert!(md.still_picture);\n    assert_eq!(md.bit_depth, 10);\n\n    let img = img.map_buf(|b| b.into_iter().map(|px| px.rgb()).collect::<Vec<_>>());\n\n    let enc = Encoder::new()\n        .with_quality(33.0)\n        .with_speed(10)\n        .with_bit_depth(BitDepth::Ten)\n        .with_alpha_quality(33.0)\n        .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty)\n        .with_num_threads(Some(1));\n\n    let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgb(img.as_ref()).unwrap();\n    assert_eq!(0, alpha_byte_size); // the image must not have alpha\n    assert!(color_byte_size > 50 && color_byte_size < 1000);\n\n    let parsed2 = avif_parse::read_avif(&mut avif_file.as_slice()).unwrap();\n\n    assert_eq!(parsed1.alpha_item, parsed2.alpha_item);\n    assert_eq!(parsed1.primary_item, parsed2.primary_item); // both are the same pixels\n}\n\n#[test]\nfn encode8_cleans_alpha() {\n    let img = imgref::ImgVec::new((0..200).flat_map(|y| (0..256).map(move |x| {\n        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))\n    })).collect(), 256, 200);\n\n    let enc = Encoder::new()\n        .with_quality(66.0)\n        .with_speed(6)\n        .with_alpha_quality(88.0)\n        .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty)\n        .with_num_threads(Some(1));\n\n    let dirty = enc\n        .encode_rgba(img.as_ref())\n        .unwrap();\n\n    let clean = enc\n        .with_alpha_color_mode(AlphaColorMode::UnassociatedClean)\n        .encode_rgba(img.as_ref())\n        .unwrap();\n\n    assert_eq!(clean.alpha_byte_size, dirty.alpha_byte_size); // same alpha on both\n    assert!(clean.alpha_byte_size > 200 && clean.alpha_byte_size < 1000);\n    assert!(clean.color_byte_size > 2000 && clean.color_byte_size < 6000);\n    assert!(clean.color_byte_size < dirty.color_byte_size / 2); // significant reduction in color data\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "use clap::{value_parser, Arg, ArgAction, Command};\nuse imgref::ImgVec;\nuse ravif::{AlphaColorMode, BitDepth, ColorModel, EncodedImage, Encoder, RGBA8};\nuse rayon::prelude::*;\nuse std::fs;\nuse std::io::{Read, Write};\nuse std::path::{Path, PathBuf};\n\ntype BoxError = Box<dyn std::error::Error + Send + Sync>;\n\nfn main() {\n    if let Err(e) = run() {\n        eprintln!(\"error: {e}\");\n        let mut source = e.source();\n        while let Some(e) = source {\n            eprintln!(\"  because: {e}\");\n            source = e.source();\n        }\n        std::process::exit(1);\n    }\n}\n\nenum MaybePath {\n    Stdio,\n    Path(PathBuf),\n}\n\nfn parse_quality(arg: &str) -> Result<f32, String> {\n    let q = arg.parse::<f32>().map_err(|e| e.to_string())?;\n    if q < 1. || q > 100. {\n        return Err(\"quality must be in 1-100 range\".into());\n    }\n    Ok(q)\n}\n\nfn parse_speed(arg: &str) -> Result<u8, String> {\n    let s = arg.parse::<u8>().map_err(|e| e.to_string())?;\n    if s < 1 || s > 100 {\n        return Err(\"speed must be in 1-10 range\".into());\n    }\n    Ok(s)\n}\n\nfn run() -> Result<(), BoxError> {\n    let args = Command::new(\"cavif-rs\")\n        .version(clap::crate_version!())\n        .author(\"Kornel Lesiński <kornel@imageoptim.com>\")\n        .about(\"Convert JPEG/PNG images to AVIF image format (based on AV1/rav1e)\")\n        .arg(Arg::new(\"quality\")\n            .short('Q')\n            .long(\"quality\")\n            .value_name(\"n\")\n            .value_parser(parse_quality)\n            .default_value(\"80\")\n            .help(\"Quality from 1 (worst) to 100 (best)\"))\n        .arg(Arg::new(\"speed\")\n            .short('s')\n            .long(\"speed\")\n            .value_name(\"n\")\n            .default_value(\"4\")\n            .value_parser(parse_speed)\n            .help(\"Encoding speed from 1 (best) to 10 (fast but ugly)\"))\n        .arg(Arg::new(\"threads\")\n            .short('j')\n            .long(\"threads\")\n            .value_name(\"n\")\n            .default_value(\"0\")\n            .value_parser(value_parser!(u8))\n            .help(\"Maximum threads to use (0 = one thread per host core)\"))\n        .arg(Arg::new(\"overwrite\")\n            .alias(\"force\")\n            .short('f')\n            .long(\"overwrite\")\n            .action(ArgAction::SetTrue)\n            .num_args(0)\n            .help(\"Replace files if there's .avif already\"))\n        .arg(Arg::new(\"output\")\n            .short('o')\n            .long(\"output\")\n            .value_parser(value_parser!(PathBuf))\n            .value_name(\"path\")\n            .help(\"Write output to this path instead of same_file.avif. It may be a file or a directory.\"))\n        .arg(Arg::new(\"quiet\")\n            .short('q')\n            .long(\"quiet\")\n            .action(ArgAction::SetTrue)\n            .num_args(0)\n            .help(\"Don't print anything\"))\n        .arg(Arg::new(\"dirty-alpha\")\n            .long(\"dirty-alpha\")\n            .action(ArgAction::SetTrue)\n            .num_args(0)\n            .help(\"Keep RGB data of fully-transparent pixels (makes larger, lower quality files)\"))\n        .arg(Arg::new(\"color\")\n            .long(\"color\")\n            .default_value(\"ycbcr\")\n            .value_parser([\"ycbcr\", \"rgb\"])\n            .help(\"Internal AVIF color model. YCbCr works better for human eyes.\"))\n        .arg(Arg::new(\"depth\")\n            .long(\"depth\")\n            .default_value(\"auto\")\n            .value_parser([\"8\", \"10\", \"auto\"])\n            .help(\"Write 8-bit (more compatible) or 10-bit (better quality) images\"))\n        .arg(Arg::new(\"IMAGES\")\n            .index(1)\n            .num_args(1..)\n            .value_parser(value_parser!(PathBuf))\n            .help(\"One or more JPEG or PNG files to convert. \\\"-\\\" is interpreted as stdin/stdout.\"))\n        .get_matches();\n\n    let output = args.get_one::<PathBuf>(\"output\").map(|s| match s {\n        s if s.as_os_str() == \"-\" => MaybePath::Stdio,\n        s => MaybePath::Path(PathBuf::from(s)),\n    });\n    let quality = *args.get_one::<f32>(\"quality\").expect(\"default\");\n    let alpha_quality = ((quality + 100.) / 2.).min(quality + quality / 4. + 2.);\n    let speed: u8 = *args.get_one::<u8>(\"speed\").expect(\"default\");\n    let overwrite = args.get_flag(\"overwrite\");\n    let quiet = args.get_flag(\"quiet\");\n    let threads = args.get_one::<u8>(\"threads\").copied();\n    let dirty_alpha = args.get_flag(\"dirty-alpha\");\n\n    let color_model = match args.get_one::<String>(\"color\").expect(\"default\").as_str() {\n        \"ycbcr\" => ColorModel::YCbCr,\n        \"rgb\" => ColorModel::RGB,\n        x => Err(format!(\"bad color type: {x}\"))?,\n    };\n\n    let depth = match args.get_one::<String>(\"depth\").expect(\"default\").as_str() {\n        \"8\" => BitDepth::Eight,\n        \"10\" => BitDepth::Ten,\n        _ => BitDepth::Auto,\n    };\n\n    let files = args.get_many::<PathBuf>(\"IMAGES\").ok_or(\"Please specify image paths to convert\")?;\n    let files: Vec<_> = files\n        .filter(|pathstr| {\n            let path = Path::new(&pathstr);\n            if let Some(s) = path.to_str() {\n                if quiet && s.parse::<u8>().is_ok() && !path.exists() {\n                    eprintln!(\"warning: -q is not for quality, so '{s}' is misinterpreted as a file. Use -Q {s}\");\n                }\n            }\n            path.extension().is_none_or(|e| if e == \"avif\" {\n                if !quiet {\n                    if path.exists() {\n                        eprintln!(\"warning: ignoring {}, because it's already an AVIF\", path.display());\n                    } else {\n                        eprintln!(\"warning: Did you mean to use -o {p}?\", p = path.display());\n                        return true;\n                    }\n                }\n                false\n            } else {\n                true\n            })\n        })\n        .map(|p| if p.as_os_str() == \"-\" {\n            MaybePath::Stdio\n        } else {\n            MaybePath::Path(PathBuf::from(p))\n        })\n        .collect();\n\n    if files.is_empty() {\n        return Err(\"No PNG/JPEG files specified\".into());\n    }\n\n    let use_dir = match output {\n        Some(MaybePath::Path(ref path)) => {\n            if files.len() > 1 {\n                let _ = fs::create_dir_all(path);\n            }\n            files.len() > 1 || path.is_dir()\n        },\n        _ => false,\n    };\n\n    let process = move |data: Vec<u8>, input_path: &MaybePath| -> Result<(), BoxError> {\n        let img = load_rgba(&data, false)?;\n        drop(data);\n        let out_path = match (&output, input_path) {\n            (None, MaybePath::Path(input)) => MaybePath::Path(input.with_extension(\"avif\")),\n            (Some(MaybePath::Path(output)), MaybePath::Path(input)) => MaybePath::Path({\n                if use_dir {\n                    output.join(Path::new(input.file_name().unwrap()).with_extension(\"avif\"))\n                } else {\n                    output.clone()\n                }\n            }),\n            (None, MaybePath::Stdio) |\n            (Some(MaybePath::Stdio), _) => MaybePath::Stdio,\n            (Some(MaybePath::Path(output)), MaybePath::Stdio) => MaybePath::Path(output.clone()),\n        };\n        match out_path {\n            MaybePath::Path(ref p) if !overwrite && p.exists() => {\n                return Err(format!(\"{} already exists; skipping\", p.display()).into());\n            },\n            _ => {},\n        }\n        let enc = Encoder::new()\n            .with_quality(quality)\n            .with_bit_depth(depth)\n            .with_speed(speed)\n            .with_alpha_quality(alpha_quality)\n            .with_internal_color_model(color_model)\n            .with_alpha_color_mode(if dirty_alpha { AlphaColorMode::UnassociatedDirty } else { AlphaColorMode::UnassociatedClean })\n            .with_num_threads(threads.filter(|&n| n > 0).map(usize::from));\n        let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgba(img.as_ref())?;\n        match out_path {\n            MaybePath::Path(ref p) => {\n                if !quiet {\n                    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);\n                }\n                fs::write(p, avif_file)\n            },\n            MaybePath::Stdio => std::io::stdout().write_all(&avif_file),\n        }\n        .map_err(|e| format!(\"Unable to write output image: {e}\"))?;\n        Ok(())\n    };\n\n    let failures = files.into_par_iter().map(|path| {\n            let tmp;\n            let (data, path_str): (_, &dyn std::fmt::Display) = match path {\n                MaybePath::Stdio => {\n                    let mut data = Vec::new();\n                    std::io::stdin().read_to_end(&mut data)?;\n                    (data, &\"stdin\")\n                },\n                MaybePath::Path(ref path) => {\n                    let data = fs::read(path).map_err(|e| format!(\"Unable to read input image {}: {e}\", path.display()))?;\n                    tmp = path.display();\n                    (data, &tmp)\n                },\n            };\n            process(data, &path)\n                .map_err(|e| BoxError::from(format!(\"{path_str}: error: {e}\")))\n        })\n        .filter_map(|res| res.err())\n        .collect::<Vec<BoxError>>();\n\n    if !failures.is_empty() {\n        if !quiet {\n            for f in failures {\n                eprintln!(\"error: {f}\");\n            }\n        }\n        std::process::exit(1);\n    }\n    Ok(())\n}\n\n#[cfg(not(feature = \"cocoa_image\"))]\nfn load_rgba(data: &[u8], premultiplied_alpha: bool) -> Result<ImgVec<RGBA8>, BoxError> {\n    use rgb::prelude::*;\n\n    let img = load_image::load_data(data)?.into_imgvec();\n    let mut img = match img {\n        load_image::export::imgref::ImgVecKind::RGB8(img) => img.map_buf(|buf| buf.into_iter().map(|px| px.with_alpha(255)).collect()),\n        load_image::export::imgref::ImgVecKind::RGBA8(img) => img,\n        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()),\n        load_image::export::imgref::ImgVecKind::RGBA16(img) => img.map_buf(|buf| buf.into_iter().map(|px| px.map(|c| (c >> 8) as u8)).collect()),\n        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()),\n        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()),\n        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()),\n        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()),\n    };\n\n    if premultiplied_alpha {\n        img.pixels_mut().for_each(|px| {\n            px.r = (u16::from(px.r) * u16::from(px.a) / 255) as u8;\n            px.g = (u16::from(px.g) * u16::from(px.a) / 255) as u8;\n            px.b = (u16::from(px.b) * u16::from(px.a) / 255) as u8;\n        });\n    }\n    Ok(img)\n}\n\n#[cfg(feature = \"cocoa_image\")]\nfn load_rgba(data: &[u8], premultiplied_alpha: bool) -> Result<ImgVec<RGBA8>, BoxError> {\n    if premultiplied_alpha {\n        Ok(cocoa_image::decode_image_as_rgba_premultiplied(data)?)\n    } else {\n        Ok(cocoa_image::decode_image_as_rgba(data)?)\n    }\n}\n"
  },
  {
    "path": "tests/stdio.rs",
    "content": "use std::io::{Read, Write};\nuse std::process::Stdio;\n\n#[test]\nfn stdio() -> Result<(), std::io::Error> {\n    let img = include_bytes!(\"testimage.png\");\n\n    let mut cmd = std::process::Command::new(env!(\"CARGO_BIN_EXE_cavif\"))\n        .stdin(Stdio::piped())\n        .stdout(Stdio::piped())\n        .arg(\"-\")\n        .arg(\"--speed=10\")\n        .spawn()?;\n\n    let mut stdin = cmd.stdin.take().unwrap();\n    let _ = std::thread::spawn(move || {\n        stdin.write_all(img).unwrap();\n    });\n\n    let mut data = Vec::new();\n    cmd.stdout.take().unwrap().read_to_end(&mut data)?;\n    assert!(cmd.wait()?.success());\n    assert_eq!(&data[4..4 + 8], b\"ftypavif\");\n    Ok(())\n}\n\n#[test]\nfn path_to_stdout() -> Result<(), std::io::Error> {\n    let mut cmd = std::process::Command::new(env!(\"CARGO_BIN_EXE_cavif\"))\n        .stdin(Stdio::null())\n        .stdout(Stdio::piped())\n        .arg(\"tests/testimage.png\")\n        .arg(\"--speed=10\")\n        .arg(\"-o\")\n        .arg(\"-\")\n        .spawn()?;\n\n    let mut data = Vec::new();\n    cmd.stdout.take().unwrap().read_to_end(&mut data)?;\n    assert!(cmd.wait()?.success());\n    avif_parse::read_avif(&mut data.as_slice()).unwrap();\n    Ok(())\n}\n"
  }
]