├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── ravif ├── Cargo.toml ├── LICENSE ├── README.md └── src │ ├── av1encoder.rs │ ├── dirtyalpha.rs │ ├── error.rs │ └── lib.rs ├── src └── main.rs └── tests ├── stdio.rs └── testimage.png /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: dtolnay/rust-toolchain@stable 11 | - uses: ilammy/setup-nasm@v1 12 | - name: Tests 13 | run: cargo test --verbose --all --all-targets 14 | - name: Check semver 15 | uses: obi1kenobi/cargo-semver-checks-action@v2 16 | with: 17 | package: ravif 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | array_width = 120 2 | binop_separator = "Back" 3 | chain_width = 120 4 | comment_width = 222 5 | condense_wildcard_suffixes = true 6 | disable_all_formatting = true 7 | edition = "2021" 8 | enum_discrim_align_threshold = 5 9 | fn_call_width = 120 10 | fn_params_layout = "Compressed" 11 | fn_single_line = false 12 | force_explicit_abi = false 13 | format_code_in_doc_comments = true 14 | imports_granularity = "Module" 15 | imports_layout = "Horizontal" 16 | match_block_trailing_comma = true 17 | max_width = 160 18 | overflow_delimited_expr = true 19 | reorder_impl_items = true 20 | single_line_if_else_max_width = 150 21 | struct_lit_width = 40 22 | use_field_init_shorthand = true 23 | use_small_heuristics = "Max" 24 | use_try_shorthand = true 25 | where_single_line = true 26 | wrap_comments = true 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cavif" 3 | description = "Encodes images in AVIF format (image2avif converter) using a pure-Rust encoder." 4 | version = "1.5.8" 5 | authors = ["Kornel Lesiński "] 6 | edition = "2021" 7 | license = "BSD-3-Clause" 8 | readme = "README.md" 9 | keywords = ["avif", "png2avif", "jpeg2avif", "convert", "av1"] 10 | categories = ["command-line-utilities", "multimedia::images", "multimedia::encoding"] 11 | homepage = "https://lib.rs/crates/cavif" 12 | repository = "https://github.com/kornelski/cavif-rs" 13 | include = ["README.md", "LICENSE", "/src/*.rs"] 14 | rust-version = "1.72" 15 | 16 | [dependencies] 17 | ravif = { version = "0.11.12", path = "./ravif", default-features = false, features = ["threading"] } 18 | rayon = "1.10.0" 19 | rgb = { version = "0.8.50", default-features = false } 20 | cocoa_image = { version = "1.0.7", optional = true } 21 | imgref = "1.11.0" 22 | clap = { version = "4.4.18", default-features = false, features = ["color", "suggestions", "wrap_help", "std", "cargo"] } 23 | load_image = "3.0.3" 24 | 25 | [features] 26 | default = ["asm", "static"] 27 | asm = ["ravif/asm"] 28 | static = ["load_image/lcms2-static"] 29 | 30 | [profile.dev] 31 | opt-level = 1 32 | debug = 1 33 | 34 | [profile.release] 35 | opt-level = 3 36 | panic = "abort" 37 | debug = false 38 | lto = true 39 | strip = true 40 | 41 | [profile.dev.package."*"] 42 | opt-level = 2 43 | 44 | [dev-dependencies] 45 | avif-parse = "1.3.2" 46 | 47 | [badges] 48 | maintenance = { status = "actively-developed" } 49 | 50 | [workspace] 51 | members = ["ravif"] 52 | 53 | [package.metadata.docs.rs] 54 | targets = ["x86_64-unknown-linux-gnu"] 55 | rustdoc-args = ["--generate-link-to-definition"] 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Kornel 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cavif` — PNG/JPEG to AVIF converter 2 | 3 | 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). 4 | 5 | ## Installation 6 | 7 | ➡️ **[Download the latest release](https://github.com/kornelski/cavif/releases)** ⬅️ 8 | 9 | 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. 10 | 11 | ## Usage 12 | 13 | Run in a terminal (hint: you don't need to type the path, terminals accept file drag'n'drop) 14 | 15 | ```bash 16 | cavif image.png 17 | ``` 18 | 19 | It makes `image.avif`. You can adjust quality (it's in 1-100 scale): 20 | 21 | ```bash 22 | cavif --quality 60 image.png 23 | ``` 24 | 25 | ### Advanced usage 26 | 27 | You can also specify multiple images. Encoding is multi-threaded, so the more, the better! 28 | 29 | ```text 30 | cavif [OPTIONS] IMAGES... 31 | ``` 32 | 33 | * `--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. 34 | * `--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. 35 | * `--overwrite` — Replace files if there's `.avif` already. By default the existing files are left untouched. 36 | * `-o path` — Write images to this path (instead of `same-name.avif`). If multiple input files are specified, it's interpreted as a directory. 37 | * `--quiet` — Don't print anything during conversion. 38 | 39 | 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: 40 | 41 | * `--dirty-alpha` — Preserve RGB values of fully transparent pixels (not recommended). By default irrelevant color of transparent pixels is cleared to avoid wasting space. 42 | * `--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. 43 | * `--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. 44 | 45 | ## Compatibility 46 | 47 | Images [work in all modern browsers](https://caniuse.com/avif). 48 | 49 | * Chrome 85+ desktop, 50 | * Chrome on Android 12, 51 | * Firefox 91, 52 | * Safari iOS 16/macOS Ventura. 53 | 54 | ### Known incompatibilities 55 | 56 | * 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. 57 | * Windows' preview doesn't seem to support 10-bit deep images. Use `--depth=8` when encoding if this is a problem. 58 | 59 | ## Building 60 | 61 | To build it from source you need Rust 1.67 or later, preferably via [rustup](https://rustup.rs). 62 | 63 | Then run in a terminal: 64 | 65 | ```bash 66 | rustup update 67 | cargo install cavif 68 | ``` 69 | -------------------------------------------------------------------------------- /ravif/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ravif" 3 | description = "rav1e-based pure Rust library for encoding images in AVIF format (powers the `cavif` tool)" 4 | version = "0.11.12" 5 | authors = ["Kornel Lesiński "] 6 | edition = "2021" 7 | license = "BSD-3-Clause" 8 | readme = "README.md" 9 | keywords = ["avif", "convert", "av1", "rav1f", "cav1f"] 10 | categories = ["multimedia::images", "multimedia::encoding"] 11 | homepage = "https://lib.rs/crates/ravif" 12 | repository = "https://github.com/kornelski/cavif-rs" 13 | include = ["README.md", "LICENSE", "Cargo.toml", "/src/*.rs"] 14 | rust-version = "1.70" # as low as `cargo -Zminimal-versions generate-lockfile` will let us go 15 | 16 | [dependencies] 17 | avif-serialize = "0.8.2" 18 | rav1e = { version = "0.7.1", default-features = false } 19 | rayon = { version = "1.10.0", optional = true } 20 | rgb = { version = "0.8.50", default-features = false } 21 | imgref = "1.11.0" 22 | loop9 = "0.1.5" 23 | quick-error = "2.0.1" 24 | 25 | [target.'cfg(target = "wasm32-unknown-unknown")'.dependencies] 26 | rav1e = { version = "0.7", default-features = false, features = ["wasm"] } 27 | 28 | [features] 29 | default = ["asm", "threading"] 30 | asm = ["rav1e/asm"] 31 | threading = ["dep:rayon", "rav1e/threading"] 32 | 33 | [profile.release] 34 | lto = true 35 | 36 | [profile.dev.package."*"] 37 | debug = false 38 | opt-level = 2 39 | 40 | [dev-dependencies] 41 | avif-parse = "1.3.2" 42 | 43 | [package.metadata.release] 44 | tag = false 45 | -------------------------------------------------------------------------------- /ravif/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /ravif/README.md: -------------------------------------------------------------------------------- 1 | # `ravif` — Pure Rust library for AVIF image encoding 2 | 3 | Encoder for AVIF images. Based on [`rav1e`](https://lib.rs/crates/rav1e) and [`avif-serialize`](https://lib.rs/crates/avif-serialize). 4 | 5 | The API is just a single `encode_rgba()` function call that spits an AVIF image. 6 | 7 | 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`. 8 | -------------------------------------------------------------------------------- /ravif/src/av1encoder.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | use crate::dirtyalpha::blurred_dirty_alpha; 3 | use crate::error::Error; 4 | #[cfg(not(feature = "threading"))] 5 | use crate::rayoff as rayon; 6 | use imgref::{Img, ImgVec}; 7 | use rav1e::prelude::*; 8 | use rgb::{RGB8, RGBA8}; 9 | 10 | /// For [`Encoder::with_internal_color_model`] 11 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 12 | pub enum ColorModel { 13 | /// Standard color model for photographic content. Usually the best choice. 14 | /// This library always uses full-resolution color (4:4:4). 15 | /// This library will automatically choose between BT.601 or BT.709. 16 | YCbCr, 17 | /// RGB channels are encoded without color space transformation. 18 | /// Usually results in larger file sizes, and is less compatible than `YCbCr`. 19 | /// Use only if the content really makes use of RGB, e.g. anaglyph images or RGB subpixel anti-aliasing. 20 | RGB, 21 | } 22 | 23 | /// Handling of color channels in transparent images. For [`Encoder::with_alpha_color_mode`] 24 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 25 | pub enum AlphaColorMode { 26 | /// Use unassociated alpha channel and leave color channels unchanged, even if there's redundant color data in transparent areas. 27 | UnassociatedDirty, 28 | /// Use unassociated alpha channel, but set color channels of transparent areas to a solid color to eliminate invisible data and improve compression. 29 | UnassociatedClean, 30 | /// Store color channels of transparent images in premultiplied form. 31 | /// This requires support for premultiplied alpha in AVIF decoders. 32 | /// 33 | /// It may reduce file sizes due to clearing of fully-transparent pixels, but 34 | /// may also increase file sizes due to creation of new edges in the color channels. 35 | /// 36 | /// Note that this is only internal detail for the AVIF file. 37 | /// It does not change meaning of `RGBA` in this library — it's always unassociated. 38 | Premultiplied, 39 | } 40 | 41 | #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] 42 | pub enum BitDepth { 43 | Eight, 44 | Ten, 45 | /// Pick 8 or 10 depending on image format and decoder compatibility 46 | #[default] 47 | Auto, 48 | } 49 | 50 | /// The newly-created image file + extra info FYI 51 | #[non_exhaustive] 52 | #[derive(Clone)] 53 | pub struct EncodedImage { 54 | /// AVIF (HEIF+AV1) encoded image data 55 | pub avif_file: Vec, 56 | /// FYI: number of bytes of AV1 payload used for the color 57 | pub color_byte_size: usize, 58 | /// FYI: number of bytes of AV1 payload used for the alpha channel 59 | pub alpha_byte_size: usize, 60 | } 61 | 62 | /// Encoder config builder 63 | #[derive(Debug, Clone)] 64 | pub struct Encoder { 65 | /// 0-255 scale 66 | quantizer: u8, 67 | /// 0-255 scale 68 | alpha_quantizer: u8, 69 | /// rav1e preset 1 (slow) 10 (fast but crappy) 70 | speed: u8, 71 | /// True if RGBA input has already been premultiplied. It inserts appropriate metadata. 72 | premultiplied_alpha: bool, 73 | /// Which pixel format to use in AVIF file. RGB tends to give larger files. 74 | color_model: ColorModel, 75 | /// How many threads should be used (0 = match core count), None - use global rayon thread pool 76 | threads: Option, 77 | /// [`AlphaColorMode`] 78 | alpha_color_mode: AlphaColorMode, 79 | /// 8 or 10 80 | output_depth: BitDepth, 81 | } 82 | 83 | /// Builder methods 84 | impl Encoder { 85 | /// Start here 86 | #[must_use] 87 | pub fn new() -> Self { 88 | Self { 89 | quantizer: quality_to_quantizer(80.), 90 | alpha_quantizer: quality_to_quantizer(80.), 91 | speed: 5, 92 | output_depth: BitDepth::default(), 93 | premultiplied_alpha: false, 94 | color_model: ColorModel::YCbCr, 95 | threads: None, 96 | alpha_color_mode: AlphaColorMode::UnassociatedClean, 97 | } 98 | } 99 | 100 | /// Quality `1..=100`. Panics if out of range. 101 | #[inline(always)] 102 | #[track_caller] 103 | #[must_use] 104 | pub fn with_quality(mut self, quality: f32) -> Self { 105 | assert!(quality >= 1. && quality <= 100.); 106 | self.quantizer = quality_to_quantizer(quality); 107 | self 108 | } 109 | 110 | #[doc(hidden)] 111 | #[deprecated(note = "Renamed to with_bit_depth")] 112 | pub fn with_depth(self, depth: Option) -> Self { 113 | self.with_bit_depth(depth.map(|d| if d >= 10 { BitDepth::Ten } else { BitDepth::Eight }).unwrap_or(BitDepth::Auto)) 114 | } 115 | 116 | /// 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. 117 | /// 118 | /// Use 8-bit depth only as a workaround for decoders that need it. 119 | /// 120 | /// This setting does not affect pixel inputs for this library. 121 | #[inline(always)] 122 | #[must_use] 123 | pub fn with_bit_depth(mut self, depth: BitDepth) -> Self { 124 | self.output_depth = depth; 125 | self 126 | } 127 | 128 | /// Quality for the alpha channel only. `1..=100`. Panics if out of range. 129 | #[inline(always)] 130 | #[track_caller] 131 | #[must_use] 132 | pub fn with_alpha_quality(mut self, quality: f32) -> Self { 133 | assert!(quality >= 1. && quality <= 100.); 134 | self.alpha_quantizer = quality_to_quantizer(quality); 135 | self 136 | } 137 | 138 | /// * 1 = very very slow, but max compression. 139 | /// * 10 = quick, but larger file sizes and lower quality. 140 | /// 141 | /// Panics if outside `1..=10`. 142 | #[inline(always)] 143 | #[track_caller] 144 | #[must_use] 145 | pub fn with_speed(mut self, speed: u8) -> Self { 146 | assert!(speed >= 1 && speed <= 10); 147 | self.speed = speed; 148 | self 149 | } 150 | 151 | /// Changes how color channels are stored in the image. The default is YCbCr. 152 | /// 153 | /// Note that this is only internal detail for the AVIF file, and doesn't 154 | /// change color model of inputs to encode functions. 155 | #[inline(always)] 156 | #[must_use] 157 | pub fn with_internal_color_model(mut self, color_model: ColorModel) -> Self { 158 | self.color_model = color_model; 159 | self 160 | } 161 | 162 | #[doc(hidden)] 163 | #[deprecated = "Renamed to `with_internal_color_model()`"] 164 | pub fn with_internal_color_space(self, color_model: ColorModel) -> Self { 165 | self.with_internal_color_model(color_model) 166 | } 167 | 168 | /// Configures `rayon` thread pool size. 169 | /// The default `None` is to use all threads in the default `rayon` thread pool. 170 | #[inline(always)] 171 | #[track_caller] 172 | #[must_use] 173 | pub fn with_num_threads(mut self, num_threads: Option) -> Self { 174 | assert!(num_threads.map_or(true, |n| n > 0)); 175 | self.threads = num_threads; 176 | self 177 | } 178 | 179 | /// Configure handling of color channels in transparent images 180 | /// 181 | /// Note that this doesn't affect input format for this library, 182 | /// which for RGBA is always uncorrelated alpha. 183 | #[inline(always)] 184 | #[must_use] 185 | pub fn with_alpha_color_mode(mut self, mode: AlphaColorMode) -> Self { 186 | self.alpha_color_mode = mode; 187 | self.premultiplied_alpha = mode == AlphaColorMode::Premultiplied; 188 | self 189 | } 190 | } 191 | 192 | /// Once done with config, call one of the `encode_*` functions 193 | impl Encoder { 194 | /// Make a new AVIF image from RGBA pixels (non-premultiplied, alpha last) 195 | /// 196 | /// Make the `Img` for the `buffer` like this: 197 | /// 198 | /// ```rust,ignore 199 | /// Img::new(&pixels_rgba[..], width, height) 200 | /// ``` 201 | /// 202 | /// If you have pixels as `u8` slice, then use the `rgb` crate, and do: 203 | /// 204 | /// ```rust,ignore 205 | /// use rgb::ComponentSlice; 206 | /// let pixels_rgba = pixels_u8.as_rgba(); 207 | /// ``` 208 | /// 209 | /// If all pixels are opaque, the alpha channel will be left out automatically. 210 | /// 211 | /// This function takes 8-bit inputs, but will generate an AVIF file using 10-bit depth. 212 | /// 213 | /// returns AVIF file with info about sizes about AV1 payload. 214 | pub fn encode_rgba(&self, in_buffer: Img<&[rgb::RGBA]>) -> Result { 215 | let new_alpha = self.convert_alpha_8bit(in_buffer); 216 | let buffer = new_alpha.as_ref().map(|b| b.as_ref()).unwrap_or(in_buffer); 217 | let use_alpha = buffer.pixels().any(|px| px.a != 255); 218 | if !use_alpha { 219 | return self.encode_rgb_internal_from_8bit(buffer.width(), buffer.height(), buffer.pixels().map(|px| px.rgb())); 220 | } 221 | 222 | let width = buffer.width(); 223 | let height = buffer.height(); 224 | let matrix_coefficients = match self.color_model { 225 | ColorModel::YCbCr => MatrixCoefficients::BT601, 226 | ColorModel::RGB => MatrixCoefficients::Identity, 227 | }; 228 | match self.output_depth { 229 | BitDepth::Eight | BitDepth::Auto => { 230 | let planes = buffer.pixels().map(|px| { 231 | let (y, u, v) = match self.color_model { 232 | ColorModel::YCbCr => rgb_to_8_bit_ycbcr(px.rgb(), BT601), 233 | ColorModel::RGB => rgb_to_8_bit_gbr(px.rgb()), 234 | }; 235 | [y, u, v] 236 | }); 237 | let alpha = buffer.pixels().map(|px| px.a); 238 | self.encode_raw_planes_8_bit(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients) 239 | }, 240 | BitDepth::Ten => { 241 | let planes = buffer.pixels().map(|px| { 242 | let (y, u, v) = match self.color_model { 243 | ColorModel::YCbCr => rgb_to_10_bit_ycbcr(px.rgb(), BT601), 244 | ColorModel::RGB => rgb_to_10_bit_gbr(px.rgb()), 245 | }; 246 | [y, u, v] 247 | }); 248 | let alpha = buffer.pixels().map(|px| to_ten(px.a)); 249 | self.encode_raw_planes_10_bit(width, height, planes, Some(alpha), PixelRange::Full, matrix_coefficients) 250 | }, 251 | } 252 | } 253 | 254 | fn convert_alpha_8bit(&self, in_buffer: Img<&[RGBA8]>) -> Option> { 255 | match self.alpha_color_mode { 256 | AlphaColorMode::UnassociatedDirty => None, 257 | AlphaColorMode::UnassociatedClean => blurred_dirty_alpha(in_buffer), 258 | AlphaColorMode::Premultiplied => { 259 | let prem = in_buffer.pixels() 260 | .map(|px| { 261 | if px.a == 0 || px.a == 255 { 262 | RGBA8::default() 263 | } else { 264 | RGBA8::new( 265 | (u16::from(px.r) * 255 / u16::from(px.a)) as u8, 266 | (u16::from(px.g) * 255 / u16::from(px.a)) as u8, 267 | (u16::from(px.b) * 255 / u16::from(px.a)) as u8, 268 | px.a, 269 | ) 270 | } 271 | }) 272 | .collect(); 273 | Some(ImgVec::new(prem, in_buffer.width(), in_buffer.height())) 274 | }, 275 | } 276 | } 277 | 278 | /// Make a new AVIF image from RGB pixels 279 | /// 280 | /// Make the `Img` for the `buffer` like this: 281 | /// 282 | /// ```rust,ignore 283 | /// Img::new(&pixels_rgb[..], width, height) 284 | /// ``` 285 | /// 286 | /// If you have pixels as `u8` slice, then first do: 287 | /// 288 | /// ```rust,ignore 289 | /// use rgb::ComponentSlice; 290 | /// let pixels_rgb = pixels_u8.as_rgb(); 291 | /// ``` 292 | /// 293 | /// returns AVIF file, size of color metadata 294 | #[inline] 295 | pub fn encode_rgb(&self, buffer: Img<&[RGB8]>) -> Result { 296 | self.encode_rgb_internal_from_8bit(buffer.width(), buffer.height(), buffer.pixels()) 297 | } 298 | 299 | fn encode_rgb_internal_from_8bit(&self, width: usize, height: usize, pixels: impl Iterator + Send + Sync) -> Result { 300 | let matrix_coefficients = match self.color_model { 301 | ColorModel::YCbCr => MatrixCoefficients::BT601, 302 | ColorModel::RGB => MatrixCoefficients::Identity, 303 | }; 304 | 305 | match self.output_depth { 306 | BitDepth::Eight => { 307 | let planes = pixels.map(|px| { 308 | let (y, u, v) = match self.color_model { 309 | ColorModel::YCbCr => rgb_to_8_bit_ycbcr(px, BT601), 310 | ColorModel::RGB => rgb_to_8_bit_gbr(px), 311 | }; 312 | [y, u, v] 313 | }); 314 | self.encode_raw_planes_8_bit(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients) 315 | }, 316 | BitDepth::Ten | BitDepth::Auto => { 317 | let planes = pixels.map(|px| { 318 | let (y, u, v) = match self.color_model { 319 | ColorModel::YCbCr => rgb_to_10_bit_ycbcr(px, BT601), 320 | ColorModel::RGB => rgb_to_10_bit_gbr(px), 321 | }; 322 | [y, u, v] 323 | }); 324 | self.encode_raw_planes_10_bit(width, height, planes, None::<[_; 0]>, PixelRange::Full, matrix_coefficients) 325 | }, 326 | } 327 | } 328 | 329 | /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`, 330 | /// with sRGB transfer characteristics and color primaries. 331 | /// 332 | /// Alpha always uses full range. Chroma subsampling is not supported, and it's a bad idea for AVIF anyway. 333 | /// If there's no alpha, use `None::<[_; 0]>`. 334 | /// 335 | /// `color_pixel_range` should be `PixelRange::Full` to avoid worsening already small 8-bit dynamic range. 336 | /// Support for limited range may be removed in the future. 337 | /// 338 | /// If `AlphaColorMode::Premultiplied` has been set, the alpha pixels must be premultiplied. 339 | /// `AlphaColorMode::UnassociatedClean` has no effect in this function, and is equivalent to `AlphaColorMode::UnassociatedDirty`. 340 | /// 341 | /// returns AVIF file, size of color metadata, size of alpha metadata overhead 342 | #[inline] 343 | pub fn encode_raw_planes_8_bit( 344 | &self, width: usize, height: usize, planes: impl IntoIterator + Send, alpha: Option + Send>, 345 | color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, 346 | ) -> Result { 347 | self.encode_raw_planes_internal(width, height, planes, alpha, color_pixel_range, matrix_coefficients, 8) 348 | } 349 | 350 | /// Encodes AVIF from 3 planar channels that are in the color space described by `matrix_coefficients`, 351 | /// with sRGB transfer characteristics and color primaries. 352 | /// 353 | /// The pixels are 10-bit (values `0.=1023`). 354 | /// 355 | /// Alpha always uses full range. Chroma subsampling is not supported, and it's a bad idea for AVIF anyway. 356 | /// If there's no alpha, use `None::<[_; 0]>`. 357 | /// 358 | /// `color_pixel_range` should be `PixelRange::Full`. Support for limited range may be removed in the future. 359 | /// 360 | /// If `AlphaColorMode::Premultiplied` has been set, the alpha pixels must be premultiplied. 361 | /// `AlphaColorMode::UnassociatedClean` has no effect in this function, and is equivalent to `AlphaColorMode::UnassociatedDirty`. 362 | /// 363 | /// returns AVIF file, size of color metadata, size of alpha metadata overhead 364 | #[inline] 365 | pub fn encode_raw_planes_10_bit( 366 | &self, width: usize, height: usize, planes: impl IntoIterator + Send, alpha: Option + Send>, 367 | color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, 368 | ) -> Result { 369 | self.encode_raw_planes_internal(width, height, planes, alpha, color_pixel_range, matrix_coefficients, 10) 370 | } 371 | 372 | #[inline(never)] 373 | fn encode_raw_planes_internal( 374 | &self, width: usize, height: usize, planes: impl IntoIterator + Send, alpha: Option + Send>, 375 | color_pixel_range: PixelRange, matrix_coefficients: MatrixCoefficients, input_pixels_bit_depth: u8, 376 | ) -> Result { 377 | let color_description = Some(ColorDescription { 378 | transfer_characteristics: TransferCharacteristics::SRGB, 379 | color_primaries: ColorPrimaries::BT709, // sRGB-compatible 380 | matrix_coefficients, 381 | }); 382 | 383 | let threads = self.threads.map(|threads| { 384 | if threads > 0 { threads } else { rayon::current_num_threads() } 385 | }); 386 | 387 | let encode_color = move || { 388 | encode_to_av1::

( 389 | &Av1EncodeConfig { 390 | width, 391 | height, 392 | bit_depth: input_pixels_bit_depth.into(), 393 | quantizer: self.quantizer.into(), 394 | speed: SpeedTweaks::from_my_preset(self.speed, self.quantizer), 395 | threads, 396 | pixel_range: color_pixel_range, 397 | chroma_sampling: ChromaSampling::Cs444, 398 | color_description, 399 | }, 400 | move |frame| init_frame_3(width, height, planes, frame), 401 | ) 402 | }; 403 | let encode_alpha = move || { 404 | alpha.map(|alpha| { 405 | encode_to_av1::

( 406 | &Av1EncodeConfig { 407 | width, 408 | height, 409 | bit_depth: input_pixels_bit_depth.into(), 410 | quantizer: self.alpha_quantizer.into(), 411 | speed: SpeedTweaks::from_my_preset(self.speed, self.alpha_quantizer), 412 | threads, 413 | pixel_range: PixelRange::Full, 414 | chroma_sampling: ChromaSampling::Cs400, 415 | color_description: None, 416 | }, 417 | |frame| init_frame_1(width, height, alpha, frame), 418 | ) 419 | }) 420 | }; 421 | #[cfg(all(target_arch = "wasm32", not(target_feature = "atomics")))] 422 | let (color, alpha) = (encode_color(), encode_alpha()); 423 | #[cfg(not(all(target_arch = "wasm32", not(target_feature = "atomics"))))] 424 | let (color, alpha) = rayon::join(encode_color, encode_alpha); 425 | let (color, alpha) = (color?, alpha.transpose()?); 426 | 427 | let avif_file = avif_serialize::Aviffy::new() 428 | .matrix_coefficients(match matrix_coefficients { 429 | MatrixCoefficients::Identity => avif_serialize::constants::MatrixCoefficients::Rgb, 430 | MatrixCoefficients::BT709 => avif_serialize::constants::MatrixCoefficients::Bt709, 431 | MatrixCoefficients::Unspecified => avif_serialize::constants::MatrixCoefficients::Unspecified, 432 | MatrixCoefficients::BT601 => avif_serialize::constants::MatrixCoefficients::Bt601, 433 | MatrixCoefficients::YCgCo => avif_serialize::constants::MatrixCoefficients::Ycgco, 434 | MatrixCoefficients::BT2020NCL => avif_serialize::constants::MatrixCoefficients::Bt2020Ncl, 435 | MatrixCoefficients::BT2020CL => avif_serialize::constants::MatrixCoefficients::Bt2020Cl, 436 | _ => return Err(Error::Unsupported("matrix coefficients")), 437 | }) 438 | .premultiplied_alpha(self.premultiplied_alpha) 439 | .to_vec(&color, alpha.as_deref(), width as u32, height as u32, input_pixels_bit_depth); 440 | let color_byte_size = color.len(); 441 | let alpha_byte_size = alpha.as_ref().map_or(0, |a| a.len()); 442 | 443 | Ok(EncodedImage { 444 | avif_file, color_byte_size, alpha_byte_size, 445 | }) 446 | } 447 | } 448 | 449 | #[inline(always)] 450 | fn to_ten(x: u8) -> u16 { 451 | (u16::from(x) << 2) | (u16::from(x) >> 6) 452 | } 453 | 454 | #[inline(always)] 455 | fn rgb_to_10_bit_gbr(px: rgb::RGB) -> (u16, u16, u16) { 456 | (to_ten(px.g), to_ten(px.b), to_ten(px.r)) 457 | } 458 | 459 | #[inline(always)] 460 | fn rgb_to_8_bit_gbr(px: rgb::RGB) -> (u8, u8, u8) { 461 | (px.g, px.b, px.r) 462 | } 463 | 464 | // const REC709: [f32; 3] = [0.2126, 0.7152, 0.0722]; 465 | const BT601: [f32; 3] = [0.2990, 0.5870, 0.1140]; 466 | 467 | #[inline(always)] 468 | fn rgb_to_ycbcr(px: rgb::RGB, depth: u8, matrix: [f32; 3]) -> (f32, f32, f32) { 469 | let max_value = ((1 << depth) - 1) as f32; 470 | let scale = max_value / 255.; 471 | let shift = (max_value * 0.5).round(); 472 | let y = scale * matrix[0] * f32::from(px.r) + scale * matrix[1] * f32::from(px.g) + scale * matrix[2] * f32::from(px.b); 473 | let cb = (f32::from(px.b) * scale - y).mul_add(0.5 / (1. - matrix[2]), shift); 474 | let cr = (f32::from(px.r) * scale - y).mul_add(0.5 / (1. - matrix[0]), shift); 475 | (y.round(), cb.round(), cr.round()) 476 | } 477 | 478 | #[inline(always)] 479 | fn rgb_to_10_bit_ycbcr(px: rgb::RGB, matrix: [f32; 3]) -> (u16, u16, u16) { 480 | let (y, u, v) = rgb_to_ycbcr(px, 10, matrix); 481 | (y as u16, u as u16, v as u16) 482 | } 483 | 484 | #[inline(always)] 485 | fn rgb_to_8_bit_ycbcr(px: rgb::RGB, matrix: [f32; 3]) -> (u8, u8, u8) { 486 | let (y, u, v) = rgb_to_ycbcr(px, 8, matrix); 487 | (y as u8, u as u8, v as u8) 488 | } 489 | 490 | fn quality_to_quantizer(quality: f32) -> u8 { 491 | let q = quality / 100.; 492 | let x = if q >= 0.85 { (1. - q) * 3. } else if q > 0.25 { 1. - 0.125 - q * 0.5 } else { 1. - q }; 493 | (x * 255.).round() as u8 494 | } 495 | 496 | #[derive(Debug, Copy, Clone)] 497 | struct SpeedTweaks { 498 | pub speed_preset: u8, 499 | 500 | pub fast_deblock: Option, 501 | pub reduced_tx_set: Option, 502 | pub tx_domain_distortion: Option, 503 | pub tx_domain_rate: Option, 504 | pub encode_bottomup: Option, 505 | pub rdo_tx_decision: Option, 506 | pub cdef: Option, 507 | /// loop restoration filter 508 | pub lrf: Option, 509 | pub sgr_complexity_full: Option, 510 | pub use_satd_subpel: Option, 511 | pub inter_tx_split: Option, 512 | pub fine_directional_intra: Option, 513 | pub complex_prediction_modes: Option, 514 | pub partition_range: Option<(u8, u8)>, 515 | pub min_tile_size: u16, 516 | } 517 | 518 | impl SpeedTweaks { 519 | pub fn from_my_preset(speed: u8, quantizer: u8) -> Self { 520 | let low_quality = quantizer < quality_to_quantizer(55.); 521 | let high_quality = quantizer > quality_to_quantizer(80.); 522 | let max_block_size = if high_quality { 16 } else { 64 }; 523 | 524 | Self { 525 | speed_preset: speed, 526 | 527 | partition_range: Some(match speed { 528 | 0 => (4, 64.min(max_block_size)), 529 | 1 if low_quality => (4, 64.min(max_block_size)), 530 | 2 if low_quality => (4, 32.min(max_block_size)), 531 | 1..=4 => (4, 16), 532 | 5..=8 => (8, 16), 533 | _ => (16, 16), 534 | }), 535 | 536 | complex_prediction_modes: Some(speed <= 1), // 2x-3x slower, 2% better 537 | sgr_complexity_full: Some(speed <= 2), // 15% slower, barely improves anything -/+1% 538 | 539 | encode_bottomup: Some(speed <= 2), // may be costly (+60%), may even backfire 540 | 541 | // big blocks disabled at 3 542 | 543 | // these two are together? 544 | rdo_tx_decision: Some(speed <= 4 && !high_quality), // it tends to blur subtle textures 545 | reduced_tx_set: Some(speed == 4 || speed >= 9), // It interacts with tx_domain_distortion too? 546 | 547 | // 4px blocks disabled at 5 548 | 549 | fine_directional_intra: Some(speed <= 6), 550 | fast_deblock: Some(speed >= 7 && !high_quality), // mixed bag? 551 | 552 | // 8px blocks disabled at 8 553 | lrf: Some(low_quality && speed <= 8), // hardly any help for hi-q images. recovers some q at low quality 554 | cdef: Some(low_quality && speed <= 9), // hardly any help for hi-q images. recovers some q at low quality 555 | 556 | inter_tx_split: Some(speed >= 9), // mixed bag even when it works, and it backfires if not used together with reduced_tx_set 557 | tx_domain_rate: Some(speed >= 10), // 20% faster, but also 10% larger files! 558 | 559 | tx_domain_distortion: None, // very mixed bag, sometimes helps speed sometimes it doesn't 560 | use_satd_subpel: Some(false), // doesn't make sense 561 | min_tile_size: match speed { 562 | 0 => 4096, 563 | 1 => 2048, 564 | 2 => 1024, 565 | 3 => 512, 566 | 4 => 256, 567 | _ => 128, 568 | } * if high_quality { 2 } else { 1 }, 569 | } 570 | } 571 | 572 | pub(crate) fn speed_settings(&self) -> SpeedSettings { 573 | let mut speed_settings = SpeedSettings::from_preset(self.speed_preset); 574 | 575 | speed_settings.multiref = false; 576 | speed_settings.rdo_lookahead_frames = 1; 577 | speed_settings.scene_detection_mode = SceneDetectionSpeed::None; 578 | speed_settings.motion.include_near_mvs = false; 579 | 580 | if let Some(v) = self.fast_deblock { speed_settings.fast_deblock = v; } 581 | if let Some(v) = self.reduced_tx_set { speed_settings.transform.reduced_tx_set = v; } 582 | if let Some(v) = self.tx_domain_distortion { speed_settings.transform.tx_domain_distortion = v; } 583 | if let Some(v) = self.tx_domain_rate { speed_settings.transform.tx_domain_rate = v; } 584 | if let Some(v) = self.encode_bottomup { speed_settings.partition.encode_bottomup = v; } 585 | if let Some(v) = self.rdo_tx_decision { speed_settings.transform.rdo_tx_decision = v; } 586 | if let Some(v) = self.cdef { speed_settings.cdef = v; } 587 | if let Some(v) = self.lrf { speed_settings.lrf = v; } 588 | if let Some(v) = self.inter_tx_split { speed_settings.transform.enable_inter_tx_split = v; } 589 | if let Some(v) = self.sgr_complexity_full { speed_settings.sgr_complexity = if v { SGRComplexityLevel::Full } else { SGRComplexityLevel::Reduced } }; 590 | if let Some(v) = self.use_satd_subpel { speed_settings.motion.use_satd_subpel = v; } 591 | if let Some(v) = self.fine_directional_intra { speed_settings.prediction.fine_directional_intra = v; } 592 | if let Some(v) = self.complex_prediction_modes { speed_settings.prediction.prediction_modes = if v { PredictionModesSetting::ComplexAll } else { PredictionModesSetting::Simple} }; 593 | if let Some((min, max)) = self.partition_range { 594 | debug_assert!(min <= max); 595 | fn sz(s: u8) -> BlockSize { 596 | match s { 597 | 4 => BlockSize::BLOCK_4X4, 598 | 8 => BlockSize::BLOCK_8X8, 599 | 16 => BlockSize::BLOCK_16X16, 600 | 32 => BlockSize::BLOCK_32X32, 601 | 64 => BlockSize::BLOCK_64X64, 602 | 128 => BlockSize::BLOCK_128X128, 603 | _ => panic!("bad size {s}"), 604 | } 605 | } 606 | speed_settings.partition.partition_range = PartitionRange::new(sz(min), sz(max)); 607 | } 608 | 609 | speed_settings 610 | } 611 | } 612 | 613 | struct Av1EncodeConfig { 614 | pub width: usize, 615 | pub height: usize, 616 | pub bit_depth: usize, 617 | pub quantizer: usize, 618 | pub speed: SpeedTweaks, 619 | /// 0 means num_cpus 620 | pub threads: Option, 621 | pub pixel_range: PixelRange, 622 | pub chroma_sampling: ChromaSampling, 623 | pub color_description: Option, 624 | } 625 | 626 | fn rav1e_config(p: &Av1EncodeConfig) -> Config { 627 | // AV1 needs all the CPU power you can give it, 628 | // except when it'd create inefficiently tiny tiles 629 | let tiles = { 630 | let threads = p.threads.unwrap_or_else(rayon::current_num_threads); 631 | threads.min((p.width * p.height) / (p.speed.min_tile_size as usize).pow(2)) 632 | }; 633 | let speed_settings = p.speed.speed_settings(); 634 | let cfg = Config::new() 635 | .with_encoder_config(EncoderConfig { 636 | width: p.width, 637 | height: p.height, 638 | time_base: Rational::new(1, 1), 639 | sample_aspect_ratio: Rational::new(1, 1), 640 | bit_depth: p.bit_depth, 641 | chroma_sampling: p.chroma_sampling, 642 | chroma_sample_position: ChromaSamplePosition::Unknown, 643 | pixel_range: p.pixel_range, 644 | color_description: p.color_description, 645 | mastering_display: None, 646 | content_light: None, 647 | enable_timing_info: false, 648 | still_picture: true, 649 | error_resilient: false, 650 | switch_frame_interval: 0, 651 | min_key_frame_interval: 0, 652 | max_key_frame_interval: 0, 653 | reservoir_frame_delay: None, 654 | low_latency: false, 655 | quantizer: p.quantizer, 656 | min_quantizer: p.quantizer as _, 657 | bitrate: 0, 658 | tune: Tune::Psychovisual, 659 | tile_cols: 0, 660 | tile_rows: 0, 661 | tiles, 662 | film_grain_params: None, 663 | level_idx: None, 664 | speed_settings, 665 | }); 666 | 667 | if let Some(threads) = p.threads { 668 | cfg.with_threads(threads) 669 | } else { 670 | cfg 671 | } 672 | } 673 | 674 | fn init_frame_3( 675 | width: usize, height: usize, planes: impl IntoIterator + Send, frame: &mut Frame

, 676 | ) -> Result<(), Error> { 677 | let mut f = frame.planes.iter_mut(); 678 | let mut planes = planes.into_iter(); 679 | 680 | // it doesn't seem to be necessary to fill padding area 681 | let mut y = f.next().unwrap().mut_slice(Default::default()); 682 | let mut u = f.next().unwrap().mut_slice(Default::default()); 683 | let mut v = f.next().unwrap().mut_slice(Default::default()); 684 | 685 | for ((y, u), v) in y.rows_iter_mut().zip(u.rows_iter_mut()).zip(v.rows_iter_mut()).take(height) { 686 | let y = &mut y[..width]; 687 | let u = &mut u[..width]; 688 | let v = &mut v[..width]; 689 | for ((y, u), v) in y.iter_mut().zip(u).zip(v) { 690 | let px = planes.next().ok_or(Error::TooFewPixels)?; 691 | *y = px[0]; 692 | *u = px[1]; 693 | *v = px[2]; 694 | } 695 | } 696 | Ok(()) 697 | } 698 | 699 | fn init_frame_1(width: usize, height: usize, planes: impl IntoIterator + Send, frame: &mut Frame

) -> Result<(), Error> { 700 | let mut y = frame.planes[0].mut_slice(Default::default()); 701 | let mut planes = planes.into_iter(); 702 | 703 | for y in y.rows_iter_mut().take(height) { 704 | let y = &mut y[..width]; 705 | for y in y.iter_mut() { 706 | *y = planes.next().ok_or(Error::TooFewPixels)?; 707 | } 708 | } 709 | Ok(()) 710 | } 711 | 712 | #[inline(never)] 713 | fn encode_to_av1(p: &Av1EncodeConfig, init: impl FnOnce(&mut Frame

) -> Result<(), Error>) -> Result, Error> { 714 | let mut ctx: Context

= rav1e_config(p).new_context()?; 715 | let mut frame = ctx.new_frame(); 716 | 717 | init(&mut frame)?; 718 | ctx.send_frame(frame)?; 719 | ctx.flush(); 720 | 721 | let mut out = Vec::new(); 722 | loop { 723 | match ctx.receive_packet() { 724 | Ok(mut packet) => match packet.frame_type { 725 | FrameType::KEY => { 726 | out.append(&mut packet.data); 727 | }, 728 | _ => continue, 729 | }, 730 | Err(EncoderStatus::Encoded) | 731 | Err(EncoderStatus::LimitReached) => break, 732 | Err(err) => Err(err)?, 733 | } 734 | } 735 | Ok(out) 736 | } 737 | -------------------------------------------------------------------------------- /ravif/src/dirtyalpha.rs: -------------------------------------------------------------------------------- 1 | use imgref::{Img, ImgRef}; 2 | use rgb::{ComponentMap, RGB, RGBA8}; 3 | 4 | #[inline] 5 | fn weighed_pixel(px: RGBA8) -> (u16, RGB) { 6 | if px.a == 0 { 7 | return (0, RGB::new(0, 0, 0)); 8 | } 9 | let weight = 256 - u16::from(px.a); 10 | (weight, RGB::new( 11 | u32::from(px.r) * u32::from(weight), 12 | u32::from(px.g) * u32::from(weight), 13 | u32::from(px.b) * u32::from(weight))) 14 | } 15 | 16 | /// Clear/change RGB components of fully-transparent RGBA pixels to make them cheaper to encode with AV1 17 | pub(crate) fn blurred_dirty_alpha(img: ImgRef) -> Option>> { 18 | // get dominant visible transparent color (excluding opaque pixels) 19 | let mut sum = RGB::new(0, 0, 0); 20 | let mut weights = 0; 21 | 22 | // Only consider colors around transparent images 23 | // (e.g. solid semitransparent area doesn't need to contribute) 24 | loop9::loop9_img(img, |_, _, top, mid, bot| { 25 | if mid.curr.a == 255 || mid.curr.a == 0 { 26 | return; 27 | } 28 | if chain(&top, &mid, &bot).any(|px| px.a == 0) { 29 | let (w, px) = weighed_pixel(mid.curr); 30 | weights += u64::from(w); 31 | sum += px.map(u64::from); 32 | } 33 | }); 34 | if weights == 0 { 35 | return None; // opaque image 36 | } 37 | 38 | let neutral_alpha = RGBA8::new((sum.r / weights) as u8, (sum.g / weights) as u8, (sum.b / weights) as u8, 0); 39 | let img2 = bleed_opaque_color(img, neutral_alpha); 40 | Some(blur_transparent_pixels(img2.as_ref())) 41 | } 42 | 43 | /// copy color from opaque pixels to transparent pixels 44 | /// (so that when edges get crushed by compression, the distortion will be away from visible edge) 45 | fn bleed_opaque_color(img: ImgRef, bg: RGBA8) -> Img> { 46 | let mut out = Vec::with_capacity(img.width() * img.height()); 47 | loop9::loop9_img(img, |_, _, top, mid, bot| { 48 | out.push(if mid.curr.a == 255 { 49 | mid.curr 50 | } else { 51 | let (weights, sum) = chain(&top, &mid, &bot) 52 | .map(|c| weighed_pixel(*c)) 53 | .fold((0u32, RGB::new(0,0,0)), |mut sum, item| { 54 | sum.0 += u32::from(item.0); 55 | sum.1 += item.1; 56 | sum 57 | }); 58 | if weights == 0 { 59 | bg 60 | } else { 61 | let mut avg = sum.map(|c| (c / weights) as u8); 62 | if mid.curr.a == 0 { 63 | avg.with_alpha(0) 64 | } else { 65 | // also change non-transparent colors, but only within range where 66 | // rounding caused by premultiplied alpha would land on the same color 67 | avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a)); 68 | avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a)); 69 | avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a)); 70 | avg.with_alpha(mid.curr.a) 71 | } 72 | } 73 | }); 74 | }); 75 | Img::new(out, img.width(), img.height()) 76 | } 77 | 78 | /// ensure there are no sharp edges created by the cleared alpha 79 | fn blur_transparent_pixels(img: ImgRef) -> Img> { 80 | let mut out = Vec::with_capacity(img.width() * img.height()); 81 | loop9::loop9_img(img, |_, _, top, mid, bot| { 82 | out.push(if mid.curr.a == 255 { 83 | mid.curr 84 | } else { 85 | let sum: RGB = chain(&top, &mid, &bot).map(|px| px.rgb().map(u16::from)).sum(); 86 | let mut avg = sum.map(|c| (c / 9) as u8); 87 | if mid.curr.a == 0 { 88 | avg.with_alpha(0) 89 | } else { 90 | // also change non-transparent colors, but only within range where 91 | // rounding caused by premultiplied alpha would land on the same color 92 | avg.r = clamp(avg.r, premultiplied_minmax(mid.curr.r, mid.curr.a)); 93 | avg.g = clamp(avg.g, premultiplied_minmax(mid.curr.g, mid.curr.a)); 94 | avg.b = clamp(avg.b, premultiplied_minmax(mid.curr.b, mid.curr.a)); 95 | avg.with_alpha(mid.curr.a) 96 | } 97 | }); 98 | }); 99 | Img::new(out, img.width(), img.height()) 100 | } 101 | 102 | #[inline(always)] 103 | fn chain<'a, T>(top: &'a loop9::Triple, mid: &'a loop9::Triple, bot: &'a loop9::Triple) -> impl Iterator + 'a { 104 | top.iter().chain(mid.iter()).chain(bot.iter()) 105 | } 106 | 107 | #[inline] 108 | fn clamp(px: u8, (min, max): (u8, u8)) -> u8 { 109 | px.max(min).min(max) 110 | } 111 | 112 | /// safe range to change px color given its alpha 113 | /// (mostly-transparent colors tolerate more variation) 114 | #[inline] 115 | fn premultiplied_minmax(px: u8, alpha: u8) -> (u8, u8) { 116 | let alpha = u16::from(alpha); 117 | let rounded = u16::from(px) * alpha / 255 * 255; 118 | 119 | // leave some spare room for rounding 120 | let low = ((rounded + 16) / alpha) as u8; 121 | let hi = ((rounded + 239) / alpha) as u8; 122 | 123 | (low.min(px), hi.max(px)) 124 | } 125 | 126 | #[test] 127 | fn preminmax() { 128 | assert_eq!((100, 100), premultiplied_minmax(100, 255)); 129 | assert_eq!((78, 100), premultiplied_minmax(100, 10)); 130 | assert_eq!(100 * 10 / 255, 78 * 10 / 255); 131 | assert_eq!(100 * 10 / 255, 100 * 10 / 255); 132 | assert_eq!((8, 119), premultiplied_minmax(100, 2)); 133 | assert_eq!((16, 239), premultiplied_minmax(100, 1)); 134 | assert_eq!((15, 255), premultiplied_minmax(255, 1)); 135 | } 136 | -------------------------------------------------------------------------------- /ravif/src/error.rs: -------------------------------------------------------------------------------- 1 | use quick_error::quick_error; 2 | 3 | #[derive(Debug)] 4 | #[doc(hidden)] 5 | pub struct EncodingErrorDetail; // maybe later 6 | 7 | quick_error! { 8 | /// Failures enum 9 | #[derive(Debug)] 10 | #[non_exhaustive] 11 | pub enum Error { 12 | /// Slices given to `encode_raw_planes` must be `width * height` large. 13 | TooFewPixels { 14 | display("Provided buffer is smaller than width * height") 15 | } 16 | Unsupported(msg: &'static str) { 17 | display("Not supported: {}", msg) 18 | } 19 | EncodingError(e: EncodingErrorDetail) { 20 | display("Encoding error reported by rav1e") 21 | from(_e: rav1e::InvalidConfig) -> (EncodingErrorDetail) 22 | from(_e: rav1e::EncoderStatus) -> (EncodingErrorDetail) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ravif/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ```rust 2 | //! use ravif::*; 3 | //! # fn doit(pixels: &[RGBA8], width: usize, height: usize) -> Result<(), Error> { 4 | //! let res = Encoder::new() 5 | //! .with_quality(70.) 6 | //! .with_speed(4) 7 | //! .encode_rgba(Img::new(pixels, width, height))?; 8 | //! std::fs::write("hello.avif", res.avif_file); 9 | //! # Ok(()) } 10 | 11 | mod av1encoder; 12 | 13 | mod error; 14 | pub use av1encoder::ColorModel; 15 | pub use error::Error; 16 | 17 | #[doc(hidden)] 18 | #[deprecated = "Renamed to `ColorModel`"] 19 | pub type ColorSpace = ColorModel; 20 | 21 | pub use av1encoder::{AlphaColorMode, BitDepth, EncodedImage, Encoder}; 22 | #[doc(inline)] 23 | pub use rav1e::prelude::MatrixCoefficients; 24 | 25 | mod dirtyalpha; 26 | 27 | #[doc(no_inline)] 28 | pub use imgref::Img; 29 | #[doc(no_inline)] 30 | pub use rgb::{RGB8, RGBA8}; 31 | 32 | #[cfg(not(feature = "threading"))] 33 | mod rayoff { 34 | pub fn current_num_threads() -> usize { 35 | std::thread::available_parallelism().map(|v| v.get()).unwrap_or(1) 36 | } 37 | 38 | pub fn join(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) { 39 | (a(), b()) 40 | } 41 | } 42 | 43 | #[test] 44 | fn encode8_with_alpha_auto() { 45 | let img = imgref::ImgVec::new((0..200).flat_map(|y| (0..256).map(move |x| { 46 | RGBA8::new(x as u8, y as u8, 255, (x + y) as u8) 47 | })).collect(), 256, 200); 48 | 49 | let enc = Encoder::new() 50 | .with_quality(22.0) 51 | .with_speed(1) 52 | .with_alpha_quality(22.0) 53 | .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty) 54 | .with_num_threads(Some(2)); 55 | let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgba(img.as_ref()).unwrap(); 56 | assert!(color_byte_size > 50 && color_byte_size < 1000); 57 | assert!(alpha_byte_size > 50 && alpha_byte_size < 1000); // the image must have alpha 58 | 59 | let parsed = avif_parse::read_avif(&mut avif_file.as_slice()).unwrap(); 60 | assert!(parsed.alpha_item.is_some()); 61 | assert!(parsed.primary_item.len() > 100); 62 | assert!(parsed.primary_item.len() < 1000); 63 | 64 | let md = parsed.primary_item_metadata().unwrap(); 65 | assert_eq!(md.max_frame_width.get(), 256); 66 | assert_eq!(md.max_frame_height.get(), 200); 67 | assert_eq!(md.bit_depth, 8); 68 | } 69 | 70 | #[test] 71 | fn encode8_opaque() { 72 | let img = imgref::ImgVec::new((0..101).flat_map(|y| (0..129).map(move |x| { 73 | RGBA8::new(255, 100 + x as u8, y as u8, 255) 74 | })).collect(), 129, 101); 75 | 76 | let enc = Encoder::new() 77 | .with_quality(33.0) 78 | .with_speed(10) 79 | .with_alpha_quality(33.0) 80 | .with_bit_depth(BitDepth::Auto) 81 | .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty) 82 | .with_num_threads(Some(1)); 83 | let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgba(img.as_ref()).unwrap(); 84 | assert_eq!(0, alpha_byte_size); // the image must not have alpha 85 | assert!(color_byte_size > 50 && color_byte_size < 1000); 86 | 87 | let parsed1 = avif_parse::read_avif(&mut avif_file.as_slice()).unwrap(); 88 | assert_eq!(None, parsed1.alpha_item); 89 | 90 | let md = parsed1.primary_item_metadata().unwrap(); 91 | assert_eq!(md.max_frame_width.get(), 129); 92 | assert_eq!(md.max_frame_height.get(), 101); 93 | assert!(md.still_picture); 94 | assert_eq!(md.bit_depth, 10); 95 | 96 | let img = img.map_buf(|b| b.into_iter().map(|px| px.rgb()).collect::>()); 97 | 98 | let enc = Encoder::new() 99 | .with_quality(33.0) 100 | .with_speed(10) 101 | .with_bit_depth(BitDepth::Ten) 102 | .with_alpha_quality(33.0) 103 | .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty) 104 | .with_num_threads(Some(1)); 105 | 106 | let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgb(img.as_ref()).unwrap(); 107 | assert_eq!(0, alpha_byte_size); // the image must not have alpha 108 | assert!(color_byte_size > 50 && color_byte_size < 1000); 109 | 110 | let parsed2 = avif_parse::read_avif(&mut avif_file.as_slice()).unwrap(); 111 | 112 | assert_eq!(parsed1.alpha_item, parsed2.alpha_item); 113 | assert_eq!(parsed1.primary_item, parsed2.primary_item); // both are the same pixels 114 | } 115 | 116 | #[test] 117 | fn encode8_cleans_alpha() { 118 | let img = imgref::ImgVec::new((0..200).flat_map(|y| (0..256).map(move |x| { 119 | 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)) 120 | })).collect(), 256, 200); 121 | 122 | let enc = Encoder::new() 123 | .with_quality(66.0) 124 | .with_speed(6) 125 | .with_alpha_quality(88.0) 126 | .with_alpha_color_mode(AlphaColorMode::UnassociatedDirty) 127 | .with_num_threads(Some(1)); 128 | 129 | let dirty = enc 130 | .encode_rgba(img.as_ref()) 131 | .unwrap(); 132 | 133 | let clean = enc 134 | .with_alpha_color_mode(AlphaColorMode::UnassociatedClean) 135 | .encode_rgba(img.as_ref()) 136 | .unwrap(); 137 | 138 | assert_eq!(clean.alpha_byte_size, dirty.alpha_byte_size); // same alpha on both 139 | assert!(clean.alpha_byte_size > 200 && clean.alpha_byte_size < 1000); 140 | assert!(clean.color_byte_size > 2000 && clean.color_byte_size < 6000); 141 | assert!(clean.color_byte_size < dirty.color_byte_size / 2); // significant reduction in color data 142 | } 143 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{value_parser, Arg, ArgAction, Command}; 2 | use imgref::ImgVec; 3 | use ravif::{AlphaColorMode, BitDepth, ColorModel, EncodedImage, Encoder, RGBA8}; 4 | use rayon::prelude::*; 5 | use std::fs; 6 | use std::io::{Read, Write}; 7 | use std::path::{Path, PathBuf}; 8 | 9 | type BoxError = Box; 10 | 11 | fn main() { 12 | if let Err(e) = run() { 13 | eprintln!("error: {e}"); 14 | let mut source = e.source(); 15 | while let Some(e) = source { 16 | eprintln!(" because: {e}"); 17 | source = e.source(); 18 | } 19 | std::process::exit(1); 20 | } 21 | } 22 | 23 | enum MaybePath { 24 | Stdio, 25 | Path(PathBuf), 26 | } 27 | 28 | fn parse_quality(arg: &str) -> Result { 29 | let q = arg.parse::().map_err(|e| e.to_string())?; 30 | if q < 1. || q > 100. { 31 | return Err("quality must be in 1-100 range".into()); 32 | } 33 | Ok(q) 34 | } 35 | 36 | fn parse_speed(arg: &str) -> Result { 37 | let s = arg.parse::().map_err(|e| e.to_string())?; 38 | if s < 1 || s > 100 { 39 | return Err("speed must be in 1-10 range".into()); 40 | } 41 | Ok(s) 42 | } 43 | 44 | fn run() -> Result<(), BoxError> { 45 | let args = Command::new("cavif-rs") 46 | .version(clap::crate_version!()) 47 | .author("Kornel Lesiński ") 48 | .about("Convert JPEG/PNG images to AVIF image format (based on AV1/rav1e)") 49 | .arg(Arg::new("quality") 50 | .short('Q') 51 | .long("quality") 52 | .value_name("n") 53 | .value_parser(parse_quality) 54 | .default_value("80") 55 | .help("Quality from 1 (worst) to 100 (best)")) 56 | .arg(Arg::new("speed") 57 | .short('s') 58 | .long("speed") 59 | .value_name("n") 60 | .default_value("4") 61 | .value_parser(parse_speed) 62 | .help("Encoding speed from 1 (best) to 10 (fast but ugly)")) 63 | .arg(Arg::new("threads") 64 | .short('j') 65 | .long("threads") 66 | .value_name("n") 67 | .default_value("0") 68 | .value_parser(value_parser!(u8)) 69 | .help("Maximum threads to use (0 = one thread per host core)")) 70 | .arg(Arg::new("overwrite") 71 | .alias("force") 72 | .short('f') 73 | .long("overwrite") 74 | .action(ArgAction::SetTrue) 75 | .num_args(0) 76 | .help("Replace files if there's .avif already")) 77 | .arg(Arg::new("output") 78 | .short('o') 79 | .long("output") 80 | .value_parser(value_parser!(PathBuf)) 81 | .value_name("path") 82 | .help("Write output to this path instead of same_file.avif. It may be a file or a directory.")) 83 | .arg(Arg::new("quiet") 84 | .short('q') 85 | .long("quiet") 86 | .action(ArgAction::SetTrue) 87 | .num_args(0) 88 | .help("Don't print anything")) 89 | .arg(Arg::new("dirty-alpha") 90 | .long("dirty-alpha") 91 | .action(ArgAction::SetTrue) 92 | .num_args(0) 93 | .help("Keep RGB data of fully-transparent pixels (makes larger, lower quality files)")) 94 | .arg(Arg::new("color") 95 | .long("color") 96 | .default_value("ycbcr") 97 | .value_parser(["ycbcr", "rgb"]) 98 | .help("Internal AVIF color model. YCbCr works better for human eyes.")) 99 | .arg(Arg::new("depth") 100 | .long("depth") 101 | .default_value("auto") 102 | .value_parser(["8", "10", "auto"]) 103 | .help("Write 8-bit (more compatible) or 10-bit (better quality) images")) 104 | .arg(Arg::new("IMAGES") 105 | .index(1) 106 | .num_args(1..) 107 | .value_parser(value_parser!(PathBuf)) 108 | .help("One or more JPEG or PNG files to convert. \"-\" is interpreted as stdin/stdout.")) 109 | .get_matches(); 110 | 111 | let output = args.get_one::("output").map(|s| { 112 | match s { 113 | s if s.as_os_str() == "-" => MaybePath::Stdio, 114 | s => MaybePath::Path(PathBuf::from(s)), 115 | } 116 | }); 117 | let quality = *args.get_one::("quality").expect("default"); 118 | let alpha_quality = ((quality + 100.) / 2.).min(quality + quality / 4. + 2.); 119 | let speed: u8 = *args.get_one::("speed").expect("default"); 120 | let overwrite = args.get_flag("overwrite"); 121 | let quiet = args.get_flag("quiet"); 122 | let threads = args.get_one::("threads").copied(); 123 | let dirty_alpha = args.get_flag("dirty-alpha"); 124 | 125 | let color_model = match args.get_one::("color").expect("default").as_str() { 126 | "ycbcr" => ColorModel::YCbCr, 127 | "rgb" => ColorModel::RGB, 128 | x => Err(format!("bad color type: {x}"))?, 129 | }; 130 | 131 | let depth = match args.get_one::("depth").expect("default").as_str() { 132 | "8" => BitDepth::Eight, 133 | "10" => BitDepth::Ten, 134 | _ => BitDepth::Auto, 135 | }; 136 | 137 | let files = args.get_many::("IMAGES").ok_or("Please specify image paths to convert")?; 138 | let files: Vec<_> = files 139 | .filter(|pathstr| { 140 | let path = Path::new(&pathstr); 141 | if let Some(s) = path.to_str() { 142 | if quiet && s.parse::().is_ok() && !path.exists() { 143 | eprintln!("warning: -q is not for quality, so '{s}' is misinterpreted as a file. Use -Q {s}"); 144 | } 145 | } 146 | path.extension().map_or(true, |e| if e == "avif" { 147 | if !quiet { 148 | if path.exists() { 149 | eprintln!("warning: ignoring {}, because it's already an AVIF", path.display()); 150 | } else { 151 | eprintln!("warning: Did you mean to use -o {p}?", p = path.display()); 152 | return true; 153 | } 154 | } 155 | false 156 | } else { 157 | true 158 | }) 159 | }) 160 | .map(|p| if p.as_os_str() == "-" { 161 | MaybePath::Stdio 162 | } else { 163 | MaybePath::Path(PathBuf::from(p)) 164 | }) 165 | .collect(); 166 | 167 | if files.is_empty() { 168 | return Err("No PNG/JPEG files specified".into()); 169 | } 170 | 171 | let use_dir = match output { 172 | Some(MaybePath::Path(ref path)) => { 173 | if files.len() > 1 { 174 | let _ = fs::create_dir_all(path); 175 | } 176 | files.len() > 1 || path.is_dir() 177 | }, 178 | _ => false, 179 | }; 180 | 181 | let process = move |data: Vec, input_path: &MaybePath| -> Result<(), BoxError> { 182 | let img = load_rgba(&data, false)?; 183 | drop(data); 184 | let out_path = match (&output, input_path) { 185 | (None, MaybePath::Path(input)) => MaybePath::Path(input.with_extension("avif")), 186 | (Some(MaybePath::Path(output)), MaybePath::Path(ref input)) => MaybePath::Path({ 187 | if use_dir { 188 | output.join(Path::new(input.file_name().unwrap()).with_extension("avif")) 189 | } else { 190 | output.clone() 191 | } 192 | }), 193 | (None, MaybePath::Stdio) | 194 | (Some(MaybePath::Stdio), _) => MaybePath::Stdio, 195 | (Some(MaybePath::Path(output)), MaybePath::Stdio) => MaybePath::Path(output.clone()), 196 | }; 197 | match out_path { 198 | MaybePath::Path(ref p) if !overwrite && p.exists() => { 199 | return Err(format!("{} already exists; skipping", p.display()).into()); 200 | }, 201 | _ => {}, 202 | } 203 | let enc = Encoder::new() 204 | .with_quality(quality) 205 | .with_bit_depth(depth) 206 | .with_speed(speed) 207 | .with_alpha_quality(alpha_quality) 208 | .with_internal_color_model(color_model) 209 | .with_alpha_color_mode(if dirty_alpha { AlphaColorMode::UnassociatedDirty } else { AlphaColorMode::UnassociatedClean }) 210 | .with_num_threads(threads.filter(|&n| n > 0).map(usize::from)); 211 | let EncodedImage { avif_file, color_byte_size, alpha_byte_size , .. } = enc.encode_rgba(img.as_ref())?; 212 | match out_path { 213 | MaybePath::Path(ref p) => { 214 | if !quiet { 215 | println!("{}: {}KB ({color_byte_size}B color, {alpha_byte_size}B alpha, {}B HEIF)", p.display(), (avif_file.len()+999)/1000, avif_file.len() - color_byte_size - alpha_byte_size); 216 | } 217 | fs::write(p, avif_file) 218 | }, 219 | MaybePath::Stdio => std::io::stdout().write_all(&avif_file), 220 | } 221 | .map_err(|e| format!("Unable to write output image: {e}"))?; 222 | Ok(()) 223 | }; 224 | 225 | let failures = files.into_par_iter().map(|path| { 226 | let tmp; 227 | let (data, path_str): (_, &dyn std::fmt::Display) = match path { 228 | MaybePath::Stdio => { 229 | let mut data = Vec::new(); 230 | std::io::stdin().read_to_end(&mut data)?; 231 | (data, &"stdin") 232 | }, 233 | MaybePath::Path(ref path) => { 234 | let data = fs::read(path).map_err(|e| format!("Unable to read input image {}: {e}", path.display()))?; 235 | tmp = path.display(); 236 | (data, &tmp) 237 | }, 238 | }; 239 | process(data, &path).map_err(|e| BoxError::from(format!("{path_str}: error: {e}"))) 240 | }) 241 | .filter_map(|res| res.err()) 242 | .collect::>(); 243 | 244 | if !failures.is_empty() { 245 | if !quiet { 246 | for f in failures { 247 | eprintln!("error: {f}"); 248 | } 249 | } 250 | std::process::exit(1); 251 | } 252 | Ok(()) 253 | } 254 | 255 | #[cfg(not(feature = "cocoa_image"))] 256 | fn load_rgba(data: &[u8], premultiplied_alpha: bool) -> Result, BoxError> { 257 | use rgb::prelude::*; 258 | 259 | let img = load_image::load_data(data)?.into_imgvec(); 260 | let mut img = match img { 261 | load_image::export::imgref::ImgVecKind::RGB8(img) => img.map_buf(|buf| buf.into_iter().map(|px| px.with_alpha(255)).collect()), 262 | load_image::export::imgref::ImgVecKind::RGBA8(img) => img, 263 | 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()), 264 | load_image::export::imgref::ImgVecKind::RGBA16(img) => img.map_buf(|buf| buf.into_iter().map(|px| px.map(|c| (c >> 8) as u8)).collect()), 265 | load_image::export::imgref::ImgVecKind::GRAY8(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = g.0; RGBA8::new(c,c,c,255) }).collect()), 266 | load_image::export::imgref::ImgVecKind::GRAY16(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = (g.0>>8) as u8; RGBA8::new(c,c,c,255) }).collect()), 267 | load_image::export::imgref::ImgVecKind::GRAYA8(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = g.0; RGBA8::new(c,c,c,g.1) }).collect()), 268 | load_image::export::imgref::ImgVecKind::GRAYA16(img) => img.map_buf(|buf| buf.into_iter().map(|g| { let c = (g.0>>8) as u8; RGBA8::new(c,c,c,(g.1>>8) as u8) }).collect()), 269 | }; 270 | 271 | if premultiplied_alpha { 272 | img.pixels_mut().for_each(|px| { 273 | px.r = (u16::from(px.r) * u16::from(px.a) / 255) as u8; 274 | px.g = (u16::from(px.g) * u16::from(px.a) / 255) as u8; 275 | px.b = (u16::from(px.b) * u16::from(px.a) / 255) as u8; 276 | }); 277 | } 278 | Ok(img) 279 | } 280 | 281 | #[cfg(feature = "cocoa_image")] 282 | fn load_rgba(data: &[u8], premultiplied_alpha: bool) -> Result, BoxError> { 283 | if premultiplied_alpha { 284 | Ok(cocoa_image::decode_image_as_rgba_premultiplied(data)?) 285 | } else { 286 | Ok(cocoa_image::decode_image_as_rgba(data)?) 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /tests/stdio.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::process::Stdio; 3 | 4 | #[test] 5 | fn stdio() -> Result<(), std::io::Error> { 6 | let img = include_bytes!("testimage.png"); 7 | 8 | let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_cavif")) 9 | .stdin(Stdio::piped()) 10 | .stdout(Stdio::piped()) 11 | .arg("-") 12 | .arg("--speed=10") 13 | .spawn()?; 14 | 15 | let mut stdin = cmd.stdin.take().unwrap(); 16 | let _ = std::thread::spawn(move || { 17 | stdin.write_all(img).unwrap(); 18 | }); 19 | 20 | let mut data = Vec::new(); 21 | cmd.stdout.take().unwrap().read_to_end(&mut data)?; 22 | assert!(cmd.wait()?.success()); 23 | assert_eq!(&data[4..4 + 8], b"ftypavif"); 24 | Ok(()) 25 | } 26 | 27 | #[test] 28 | fn path_to_stdout() -> Result<(), std::io::Error> { 29 | let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_cavif")) 30 | .stdin(Stdio::null()) 31 | .stdout(Stdio::piped()) 32 | .arg("tests/testimage.png") 33 | .arg("--speed=10") 34 | .arg("-o") 35 | .arg("-") 36 | .spawn()?; 37 | 38 | let mut data = Vec::new(); 39 | cmd.stdout.take().unwrap().read_to_end(&mut data)?; 40 | assert!(cmd.wait()?.success()); 41 | avif_parse::read_avif(&mut data.as_slice()).unwrap(); 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /tests/testimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kornelski/cavif-rs/7d3801cde205db9adcc7a44425f6ef69a914d5a7/tests/testimage.png --------------------------------------------------------------------------------