From bd06494cc87e0c7796353a3b229c680660d39502 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Wed, 5 May 2021 08:14:06 -0700 Subject: [PATCH 1/5] misc: improve avif limited/full testing --- refract/src/image.rs | 59 ++------------ refract_core/src/enc/avif.rs | 49 ++++++++---- refract_core/src/enc/iter.rs | 62 +++++++++++---- refract_core/src/image/mod.rs | 143 ---------------------------------- refract_core/src/lib.rs | 20 +++-- refract_core/src/source.rs | 9 +-- skel/reference.txt | 58 +++++++++----- 7 files changed, 137 insertions(+), 263 deletions(-) diff --git a/refract/src/image.rs b/refract/src/image.rs index 696f1c8..4069236 100644 --- a/refract/src/image.rs +++ b/refract/src/image.rs @@ -9,7 +9,6 @@ use dactyl::{ }; use fyi_msg::Msg; use refract_core::{ - FLAG_AVIF_LIMITED, Output, OutputKind, RefractError, @@ -38,7 +37,6 @@ pub(super) struct ImageCli<'a> { kind: OutputKind, tmp: PathBuf, dst: PathBuf, - flags: u8, } impl<'a> Drop for ImageCli<'a> { @@ -65,35 +63,18 @@ impl<'a> ImageCli<'a> { let _res = std::fs::File::create(&tmp); } - // Default limited mode for AVIF when appropriate. If no candidate is - // chosen, the process will repeat in full RGB mode. - let flags: u8 = - if kind == OutputKind::Avif && src.supports_yuv_limited() { - FLAG_AVIF_LIMITED - } - else { 0 }; - Self { src, kind, tmp, dst, - flags, } } /// # Encode. - pub(crate) fn encode(mut self) { + pub(crate) fn encode(self) { // Print a header for the encoding type. - println!( - "\x1b[34m[\x1b[96;1m{}\x1b[0;34m]\x1b[0m{}", - self.kind, - // Append a subtitle for limited-range AVIF. - if FLAG_AVIF_LIMITED == self.flags & FLAG_AVIF_LIMITED { - " \x1b[2m(YCbCr)\x1b[0m" - } - else { "" } - ); + println!("\x1b[34m[\x1b[96;1m{}\x1b[0;34m]\x1b[0m", self.kind); // We'll be re-using this prompt throughout. let prompt = Msg::plain(format!( @@ -104,7 +85,7 @@ impl<'a> ImageCli<'a> { .with_indent(1); // Loop it. - let mut guide = self.src.encode(self.kind, self.flags); + let mut guide = self.src.encode(self.kind, 0); while guide.advance() .and_then(|data| save_image(&self.tmp, data).ok()) .is_some() @@ -119,7 +100,7 @@ impl<'a> ImageCli<'a> { // Wrap it up! let time = guide.time(); - let res = self.finish(guide.take()); + self.finish(guide.take()); // Print the timings. Msg::plain(format!( @@ -128,33 +109,17 @@ impl<'a> ImageCli<'a> { )) .with_indent(1) .print(); - - // We might want to re-run AVIF in full mode. This only applies if no - // candidate was found using YCbCr. - if - ! res && - self.kind == OutputKind::Avif && - FLAG_AVIF_LIMITED == self.flags & FLAG_AVIF_LIMITED - { - self.flags = 0; - self.encode() - } } /// # Finish. - fn finish(&self, result: Result) -> bool { + fn finish(self, result: Result) { // Handle results. match result { Ok(result) => match save_image(&self.dst, &result) { Ok(_) => print_success(self.src.size().get(), &result, &self.dst), Err(e) => print_error(e), }, - Err(e) => { - if self.dst.exists() { - let _res = std::fs::remove_file(&self.dst); - } - print_error(e) - } + Err(e) => print_error(e), } } } @@ -183,20 +148,14 @@ pub(super) fn print_path_title(path: &Path) { } /// # Print Error. -/// -/// This always returns false. -fn print_error(err: RefractError) -> bool { +fn print_error(err: RefractError) { Msg::warning(err.as_str()) .with_indent(1) .print(); - - false } /// # Print Success. -/// -/// This always returns true. -fn print_success(src_size: u64, output: &Output, dst_path: &Path) -> bool { +fn print_success(src_size: u64, output: &Output, dst_path: &Path) { let diff: u64 = src_size - output.size().get(); let per = dactyl::int_div_float(diff, src_size); let name = dst_path.file_name() @@ -224,8 +183,6 @@ fn print_success(src_size: u64, output: &Output, dst_path: &Path) -> bool { } ) .print(); - - true } /// # Write Result. diff --git a/refract_core/src/enc/avif.rs b/refract_core/src/enc/avif.rs index 1add4ee..e6c9164 100644 --- a/refract_core/src/enc/avif.rs +++ b/refract_core/src/enc/avif.rs @@ -4,11 +4,14 @@ use crate::{ Candidate, + FLAG_AVIF_LIMITED, + FLAG_AVIF_SLOW, Image, RefractError, }; use libavif_sys::{ AVIF_CHROMA_SAMPLE_POSITION_COLOCATED, + AVIF_CHROMA_UPSAMPLING_BILINEAR, AVIF_CODEC_CHOICE_RAV1E, AVIF_COLOR_PRIMARIES_BT709, AVIF_MATRIX_COEFFICIENTS_BT709, @@ -18,6 +21,7 @@ use libavif_sys::{ AVIF_RANGE_FULL, AVIF_RANGE_LIMITED, AVIF_RESULT_OK, + AVIF_RGB_FORMAT_RGBA, AVIF_TRANSFER_CHARACTERISTICS_SRGB, avifEncoder, avifEncoderCreate, @@ -26,7 +30,9 @@ use libavif_sys::{ avifImage, avifImageCreate, avifImageDestroy, + avifImageRGBToYUV, avifResult, + avifRGBImage, avifRWData, avifRWDataFree, }; @@ -101,20 +107,36 @@ impl Drop for LibAvifEncoder { /// garbage cleanup. struct LibAvifImage(*mut avifImage); -impl TryFrom<&Image<'_>> for LibAvifImage { - type Error = RefractError; - +impl LibAvifImage { #[allow(clippy::cast_possible_truncation)] // The values are purpose-made. - fn try_from(src: &Image) -> Result { + fn new(src: &Image, flags: u8) -> Result { + // Make sure dimensions fit u32. + let width = src.width_u32()?; + let height = src.height_u32()?; + // AVIF dimensions can't exceed this amount. We might as well bail as // early as possible. if src.width() * src.height() > 16_384 * 16_384 { return Err(RefractError::Overflow); } - let limited = src.is_yuv_limited(); + let limited = FLAG_AVIF_LIMITED == flags & FLAG_AVIF_LIMITED; let greyscale: bool = src.color_kind().is_greyscale(); + // Make an "avifRGBImage" from our buffer. + let raw: &[u8] = &*src; + let rgb = avifRGBImage { + width, + height, + depth: 8, + format: AVIF_RGB_FORMAT_RGBA, + chromaUpsampling: AVIF_CHROMA_UPSAMPLING_BILINEAR, + ignoreAlpha: ! src.color_kind().has_alpha() as _, + alphaPremultiplied: 0, + pixels: raw.as_ptr() as *mut u8, + rowBytes: 4 * width, + }; + // And convert it to YUV. let yuv = unsafe { let tmp = avifImageCreate( @@ -128,19 +150,10 @@ impl TryFrom<&Image<'_>> for LibAvifImage { // This shouldn't happen, but could, maybe. if tmp.is_null() { return Err(RefractError::Encode); } - let (yuv_planes, yuv_row_bytes, alpha_plane, alpha_row_bytes) = src.yuv(); - - (*tmp).imageOwnsYUVPlanes = 0; (*tmp).yuvRange = if limited { AVIF_RANGE_LIMITED } else { AVIF_RANGE_FULL }; - (*tmp).yuvPlanes = yuv_planes; - (*tmp).yuvRowBytes = yuv_row_bytes; - - (*tmp).imageOwnsAlphaPlane = 0; (*tmp).alphaRange = AVIF_RANGE_FULL; - (*tmp).alphaPlane = alpha_plane; - (*tmp).alphaRowBytes = alpha_row_bytes; (*tmp).yuvChromaSamplePosition = AVIF_CHROMA_SAMPLE_POSITION_COLOCATED; (*tmp).colorPrimaries = AVIF_COLOR_PRIMARIES_BT709 as _; @@ -149,6 +162,8 @@ impl TryFrom<&Image<'_>> for LibAvifImage { if greyscale || limited { AVIF_MATRIX_COEFFICIENTS_BT709 as _ } else { AVIF_MATRIX_COEFFICIENTS_IDENTITY as _ }; + maybe_die(avifImageRGBToYUV(tmp, &rgb))?; + tmp }; @@ -287,13 +302,13 @@ pub(super) fn make_lossy( img: &Image, candidate: &mut Candidate, quality: NonZeroU8, - tiling: bool + flags: u8 ) -> Result<(), RefractError> { - let image = LibAvifImage::try_from(img)?; + let image = LibAvifImage::new(img, flags)?; let encoder = LibAvifEncoder::try_from(quality)?; // Configure tiling. - if tiling { + if 0 == FLAG_AVIF_SLOW & flags { if let Some((x, y)) = tiles(img.width(), img.height()) { unsafe { (*encoder.0).tileRowsLog2 = x; diff --git a/refract_core/src/enc/iter.rs b/refract_core/src/enc/iter.rs index ef27c75..d5f081c 100644 --- a/refract_core/src/enc/iter.rs +++ b/refract_core/src/enc/iter.rs @@ -4,6 +4,10 @@ use crate::{ Candidate, + FLAG_AVIF_LIMITED, + FLAG_AVIF_SLOW, + FLAG_DONE, + FLAG_NO_AVIF_LIMITED, Image, Output, OutputKind, @@ -61,7 +65,7 @@ pub struct EncodeIter<'a> { candidate: Candidate, time: Duration, - done: bool, + flags: u8, } impl<'a> EncodeIter<'a> { @@ -69,9 +73,20 @@ impl<'a> EncodeIter<'a> { /// # New. /// /// Start a new iterator with a given source and output format. - pub(crate) fn new(src: &'a Source<'a>, kind: OutputKind, flags: u8) -> Self { + pub(crate) fn new(src: &'a Source<'a>, kind: OutputKind, mut flags: u8) -> Self { let (bottom, top) = kind.quality_range(); + // Clean up flags. + if kind == OutputKind::Avif { + if src.supports_yuv_limited() && 0 == flags & FLAG_NO_AVIF_LIMITED { + flags |= FLAG_AVIF_LIMITED; + } + } + // Only AVIF has flags right now. + else { + flags = 0; + } + let mut out = Self { bottom, top, @@ -80,10 +95,8 @@ impl<'a> EncodeIter<'a> { src: match kind { // JPEG XL takes a compacted source. OutputKind::Jxl => src.img_compact(), - // AVIF wants some kind of YUV source. - OutputKind::Avif => src.img_yuv(flags), - // WebP always wants RGB. - OutputKind::Webp => src.img(), + // AVIF and WebP work from full buffers. + OutputKind::Avif | OutputKind::Webp => src.img(), }, size: src.size(), kind, @@ -92,7 +105,7 @@ impl<'a> EncodeIter<'a> { candidate: Candidate::new(kind), time: Duration::from_secs(0), - done: false, + flags }; // Try lossless compression. @@ -146,15 +159,19 @@ impl EncodeIter<'_> { /// /// When `main == false`, this will also return an error if the encoder /// does not require a final pass. - fn lossy(&mut self, quality: NonZeroU8, main: bool) -> Result<(), RefractError> { + fn lossy(&mut self, quality: NonZeroU8, flags: u8) -> Result<(), RefractError> { self.candidate.set_quality(Some(quality)); + // No tiling is done as a final pass at the end; it only applies to + // AVIF sessions. + let main = 0 == flags & FLAG_AVIF_SLOW; + match self.kind { OutputKind::Avif => super::avif::make_lossy( &self.src, &mut self.candidate, quality, - main, + flags, ), OutputKind::Jxl if main => super::jxl::make_lossy( &self.src, @@ -187,12 +204,26 @@ impl EncodeIter<'_> { let now = Instant::now(); // Handle the actual next business. - let res = self.next_inner(); + let mut res = self.next_inner(); // If we're done, see if it is worth doing one more (silent) pass // against the best found. This currently only applies to AVIF. - if res.is_none() && ! self.done { + if res.is_none() && 0 == self.flags & FLAG_DONE { let _res = self.next_final(); + + // We might need to reboot and run again in full mode if this run + // was done in limited mode. (Limited is usually but not always + // better.) + if FLAG_AVIF_LIMITED == self.flags & FLAG_AVIF_LIMITED { + let (bottom, top) = self.kind.quality_range(); + self.bottom = bottom; + self.top = top; + self.tried.clear(); + self.flags &= ! FLAG_AVIF_LIMITED; + + // And run again. + res = self.next_inner(); + } } // Record the time spent. @@ -220,6 +251,7 @@ impl EncodeIter<'_> { pub fn keep(&mut self) { self.set_top(self.candidate.quality()); self.keep_candidate(); + self.flags &= ! FLAG_DONE; } /// # Keep Candidate. @@ -247,7 +279,7 @@ impl EncodeIter<'_> { /// time. fn next_inner(&mut self) -> Option<()> { let quality = self.next_quality()?; - match self.lossy(quality, true) { + match self.lossy(quality, self.flags) { Ok(_) => Some(()), Err(RefractError::TooBig) => { // Recurse to see if the next-next quality works out OK. @@ -277,15 +309,15 @@ impl EncodeIter<'_> { /// gains, etc., but the result is not actually used anywhere. If it works /// it is silently saved, if not, no changes occur. fn next_final(&mut self) -> Result<(), RefractError> { - if self.done { return Ok(()); } - self.done = true; + if FLAG_DONE == self.flags & FLAG_DONE { return Ok(()); } + self.flags |= FLAG_DONE; let quality = self.best .as_ref() .map(Output::quality) .ok_or(RefractError::NothingDoing)?; - self.lossy(quality, false)?; + self.lossy(quality, self.flags | FLAG_AVIF_SLOW)?; self.keep_candidate(); Ok(()) diff --git a/refract_core/src/image/mod.rs b/refract_core/src/image/mod.rs index aaf7a30..c3f48b9 100644 --- a/refract_core/src/image/mod.rs +++ b/refract_core/src/image/mod.rs @@ -8,7 +8,6 @@ pub(super) mod pixel; use crate::{ ColorKind, - FLAG_AVIF_LIMITED, PixelKind, RefractError, SourceKind, @@ -352,127 +351,6 @@ impl<'a> Image<'a> { /// # YUV. impl<'a> Image<'a> { - #[must_use] - /// # As YUV. - /// - /// This converts a [`PixelKind::Full`] RGBA image into a YUV one. - /// - /// The internal buffer is filled with all the Ys first, then the Us, then - /// the Vs, and finally the As. - /// - /// Depending on the flags and source, this will either create a buffer in - /// limited range `YCbCr` format or a simple passthrough `GBR` one for full- - /// range encoding. - /// - /// This is only used for AVIF encoding and because of its specificity, is - /// only exposed to this crate. (It would be too easy to misuse elsewhere.) - pub(crate) fn as_yuv(&'a self, flags: u8) -> Self { - debug_assert_eq!(self.pixel, PixelKind::Full); - - let size = self.width.get() * self.height.get(); - - let mut y_plane: Vec = Vec::with_capacity(size); - let mut u_plane: Vec = Vec::with_capacity(size); - let mut v_plane: Vec = Vec::with_capacity(size); - let mut a_plane: Vec = Vec::with_capacity(size); - - let limited = self.wants_yuv_limited(flags); - - self.img.chunks_exact(4).for_each(|rgba| { - if limited { - let r = f32::from(rgba[0]); - let g = f32::from(rgba[1]); - let b = f32::from(rgba[2]); - - let y = r.mul_add(0.2126, g.mul_add(0.7152, 0.0722 * b)); - let cb = (b - y) * (0.5 / (1.0 - 0.0722)); - let cr = (r - y) * (0.5 / (1.0 - 0.2126)); - - y_plane.push(normalize_yuv_pixel(y * (235.0 - 16.0) / 255.0 + 16.0)); - u_plane.push(normalize_yuv_pixel((cb + 128.0) * (240.0 - 16.0) / 255.0 + 16.0)); - v_plane.push(normalize_yuv_pixel((cr + 128.0) * (240.0 - 16.0) / 255.0 + 16.0)); - } - else { - y_plane.push(rgba[1]); // G. - u_plane.push(rgba[2]); // B. - v_plane.push(rgba[0]); // R. - } - - // Alpha is always just alpha. - a_plane.push(rgba[3]); - }); - - // Take over the y_plane and add the rest of the data to it. - y_plane.append(&mut u_plane); - y_plane.append(&mut v_plane); - y_plane.append(&mut a_plane); - - // Triple check the math. - debug_assert_eq!(y_plane.len(), size * 4); - - Self { - img: Cow::Owned(y_plane), - color: self.color, - pixel: - if limited { PixelKind::YuvLimited } - else { PixelKind::YuvFull }, - width: self.width, - height: self.height, - stride: self.stride, - } - } - - /// # YUV Plane Pointers. - /// - /// Return pointers and sizes for YUV/alpha data for AVIF encoding. - /// - /// This method only applies for images with pixel types [`PixelKind::YuvFull`] - /// and [`PixelKind::YuvLimited`]. - /// - /// This is only used for AVIF encoding and because of its specificity, is - /// only exposed to this crate. (It would be too easy to misuse elsewhere.) - /// - /// ## Safety - /// - /// This method itself is safe, but returns mutable pointers that if - /// misused would cause trouble. - pub(crate) unsafe fn yuv(&'a self) -> ([*mut u8; 3], [u32; 3], *mut u8, u32) { - // We must be a YUV type. - debug_assert!(matches!(self.pixel, PixelKind::YuvFull | PixelKind::YuvLimited)); - - let size = self.width.get() * self.height.get(); - - // Note: these pixels aren't really mutated. - let ptr = self.img.as_ptr(); - let yuv_ptr = [ - ptr as *mut u8, // Y. - ptr.add(size) as *mut u8, // U. - ptr.add(size * 2) as *mut u8, // V. - ]; - - let a_ptr = ptr.add(size * 3) as *mut u8; - - // This won't fail because width fits in i32. - let width32 = self.width_u32().unwrap(); - - ( - yuv_ptr, - [width32, width32, width32], // Row bytes, 1 x width. - a_ptr, - if self.color.has_alpha() { width32 } // Row bytes as above. - else { 0 } // Or none if no alpha. - ) - } - - #[inline] - /// # Is `YCbCr` YUV? - /// - /// This is just a simple convenience method, equivalent to checking the - /// pixel type. - pub(crate) fn is_yuv_limited(&self) -> bool { - self.pixel == PixelKind::YuvLimited - } - /// # Can YUV? /// /// This is a convenience function that will evaluate whether an image @@ -481,25 +359,4 @@ impl<'a> Image<'a> { pub(crate) fn supports_yuv_limited(&self) -> bool { self.pixel == PixelKind::Full && self.color.is_color() } - - /// # Wants `YCbCr` YUV? - /// - /// This is a convenience function that will evaluate the image and flags - /// to see if it should be limited-range YUV. - pub(crate) fn wants_yuv_limited(&self, flags: u8) -> bool { - self.supports_yuv_limited() && FLAG_AVIF_LIMITED == flags & FLAG_AVIF_LIMITED - } -} - - - -#[allow(clippy::cast_possible_truncation)] // Values are clamped. -#[allow(clippy::cast_sign_loss)] // Values are clamped. -#[inline] -/// # Normalize YUV Value. -/// -/// This simply rounds and converts the working float pixel values into u8 for -/// storage. -fn normalize_yuv_pixel(pix: f32) -> u8 { - pix.round().max(0.0).min(255.0) as u8 } diff --git a/refract_core/src/lib.rs b/refract_core/src/lib.rs index 7f27ac7..7668111 100644 --- a/refract_core/src/lib.rs +++ b/refract_core/src/lib.rs @@ -51,12 +51,16 @@ pub use source::{ -/// # Flag: AVIF Limited. +/// # Flag: Disable AVIF Limited Range /// -/// When enabled, color RGB sources will be encoded using the limited `YCbCr` -/// color space. This typically leads to smaller output compared to the default -/// full-range RGB mode. -/// -/// This flag has no effect on greyscale images, which are always encoded using -/// the full range. -pub const FLAG_AVIF_LIMITED: u8 = 0b0001; +/// When set, limited ranges will never be tested. +pub const FLAG_NO_AVIF_LIMITED: u8 = 0b0001; + +/// # Internal Flag: AVIF Limited. +pub(crate) const FLAG_AVIF_LIMITED: u8 = 0b0010; + +/// # Internal Flag: AVIF Slow Encoding (no tiling). +pub(crate) const FLAG_AVIF_SLOW: u8 = 0b0100; + +/// # Internal Flag: Done w/ Encoding. +pub(crate) const FLAG_DONE: u8 = 0b1000; diff --git a/refract_core/src/source.rs b/refract_core/src/source.rs index 5881d64..ee81896 100644 --- a/refract_core/src/source.rs +++ b/refract_core/src/source.rs @@ -141,13 +141,6 @@ impl Source<'_> { /// Return a compacted version of the image buffer. pub fn img_compact(&self) -> Image<'_> { self.img.as_compact() } - #[must_use] - /// # YUV Image (reference). - /// - /// Return an image buffer converted to YUV range, either limited or full - /// depending on the flag and source colorness. - pub(crate) fn img_yuv(&self, flags: u8) -> Image<'_> { self.img.as_yuv(flags) } - #[must_use] /// # Path. /// @@ -166,7 +159,7 @@ impl Source<'_> { /// /// This is a convenient function that will evaluate whether an image /// source supports limited-range YUV encoding. - pub fn supports_yuv_limited(&self) -> bool { self.img.supports_yuv_limited() } + pub(crate) fn supports_yuv_limited(&self) -> bool { self.img.supports_yuv_limited() } } /// ## Encoding. diff --git a/skel/reference.txt b/skel/reference.txt index ec123fd..f67a44a 100644 --- a/skel/reference.txt +++ b/skel/reference.txt @@ -15,8 +15,8 @@ Does bars.png.PROPOSED.avif look good? [y/N] Does bars.png.PROPOSED.avif look good? [y/N] y Does bars.png.PROPOSED.avif look good? [y/N] - Does bars.png.PROPOSED.avif look good? [y/N] y - Success: Created bars.png.avif with quantizer 21. (Saved 197,047 bytes, 88.22%.) + Does bars.png.PROPOSED.avif look good? [y/N] + Success: Created bars.png.avif with quantizer 20. (Saved 195,719 bytes, 87.62%.) [JPEG XL] Does bars.png.PROPOSED.jxl look good? [y/N] Does bars.png.PROPOSED.jxl look good? [y/N] @@ -39,14 +39,21 @@ Does cats.jpg.PROPOSED.webp look good? [y/N] Does cats.jpg.PROPOSED.webp look good? [y/N] y Success: Created cats.jpg.webp with quality 49. (Saved 324,547 bytes, 73.09%.) -[AVIF] (YCbCr) +[AVIF] + Does cats.jpg.PROPOSED.avif look good? [y/N] Does cats.jpg.PROPOSED.avif look good? [y/N] y Does cats.jpg.PROPOSED.avif look good? [y/N] Does cats.jpg.PROPOSED.avif look good? [y/N] + Does cats.jpg.PROPOSED.avif look good? [y/N] y Does cats.jpg.PROPOSED.avif look good? [y/N] Does cats.jpg.PROPOSED.avif look good? [y/N] y + Does cats.jpg.PROPOSED.avif look good? [y/N] + Does cats.jpg.PROPOSED.avif look good? [y/N] + Does cats.jpg.PROPOSED.avif look good? [y/N] + Does cats.jpg.PROPOSED.avif look good? [y/N] Does cats.jpg.PROPOSED.avif look good? [y/N] y - Success: Created cats.jpg.avif with quantizer 34. (Saved 338,557 bytes, 76.24%.) + Success: Created cats.jpg.avif with quantizer 32. (Saved 224,518 bytes, 50.56%.) + Total computation time: 5 minutes and 5 seconds. [JPEG XL] Does cats.jpg.PROPOSED.jxl look good? [y/N] Does cats.jpg.PROPOSED.jxl look good? [y/N] @@ -70,14 +77,19 @@ Does circles.jpg.PROPOSED.webp look good? [y/N] y Does circles.jpg.PROPOSED.webp look good? [y/N] y Success: Created circles.jpg.webp with quality 97. (Saved 38,001 bytes, 51.64%.) -[AVIF] (YCbCr) +[AVIF] Does circles.jpg.PROPOSED.avif look good? [y/N] y Does circles.jpg.PROPOSED.avif look good? [y/N] Does circles.jpg.PROPOSED.avif look good? [y/N] Does circles.jpg.PROPOSED.avif look good? [y/N] Does circles.jpg.PROPOSED.avif look good? [y/N] Does circles.jpg.PROPOSED.avif look good? [y/N] y - Success: Created circles.jpg.avif with quantizer 32. (Saved 64,629 bytes, 87.83%.) + Does circles.jpg.PROPOSED.avif look good? [y/N] + Does circles.jpg.PROPOSED.avif look good? [y/N] + Does circles.jpg.PROPOSED.avif look good? [y/N] + Does circles.jpg.PROPOSED.avif look good? [y/N] + Does circles.jpg.PROPOSED.avif look good? [y/N] + Success: Created circles.jpg.avif with quantizer 32. (Saved 64,623 bytes, 87.82%.) [JPEG XL] Does circles.jpg.PROPOSED.jxl look good? [y/N] Does circles.jpg.PROPOSED.jxl look good? [y/N] @@ -123,14 +135,16 @@ +------------------------------------------+ [WebP] Success: Created mountains-on-mars.png.webp with lossless quality. (Saved 5,882 bytes, 31.56%.) -[AVIF] (YCbCr) +[AVIF] Does mountains-on-mars.png.PROPOSED.avif look good? [y/N] Does mountains-on-mars.png.PROPOSED.avif look good? [y/N] Does mountains-on-mars.png.PROPOSED.avif look good? [y/N] Does mountains-on-mars.png.PROPOSED.avif look good? [y/N] y Does mountains-on-mars.png.PROPOSED.avif look good? [y/N] y + Does mountains-on-mars.png.PROPOSED.avif look good? [y/N] y Does mountains-on-mars.png.PROPOSED.avif look good? [y/N] - Success: Created mountains-on-mars.png.avif with quantizer 6. (Saved 8,869 bytes, 47.59%.) + Does mountains-on-mars.png.PROPOSED.avif look good? [y/N] + Success: Created mountains-on-mars.png.avif with quantizer 7. (Saved 8,936 bytes, 47.95%.) [JPEG XL] Does mountains-on-mars.png.PROPOSED.jxl look good? [y/N] Does mountains-on-mars.png.PROPOSED.jxl look good? [y/N] @@ -171,8 +185,6 @@ +----------------------------+ [WebP] Warning: No acceptable WebP candidate was found. -[AVIF] (YCbCr) - Warning: No acceptable AVIF candidate was found. [AVIF] Warning: No acceptable AVIF candidate was found. [JPEG XL] @@ -183,10 +195,8 @@ +--------------------------+ [WebP] Success: Created r.png.webp with lossless quality. (Saved 256 bytes, 42.24%.) -AVIF] (YCbCr) - Does r.png.PROPOSED.avif look good? [y/N] - Warning: No acceptable AVIF candidate was found. [AVIF] + Does r.png.PROPOSED.avif look good? [y/N] Does r.png.PROPOSED.avif look good? [y/N] Warning: No acceptable AVIF candidate was found. [JPEG XL] @@ -203,14 +213,17 @@ AVIF] (YCbCr) Does statler_waldorf_cutout.png.PROPOSED.webp look good? [y/N] y Does statler_waldorf_cutout.png.PROPOSED.webp look good? [y/N] y Success: Created statler_waldorf_cutout.png.webp with quality 82. (Saved 231,997 bytes, 90.52%.) -[AVIF] (YCbCr) - Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] - Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] y +[AVIF] Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] y Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] y - Success: Created statler_waldorf_cutout.png.avif with quantizer 19. (Saved 233,193 bytes, 90.98%.) + Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] y + Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] + Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] + Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] + Does statler_waldorf_cutout.png.PROPOSED.avif look good? [y/N] + Success: Created statler_waldorf_cutout.png.avif with quantizer 14. (Saved 227,053 bytes, 88.59%.) [JPEG XL] Does statler_waldorf_cutout.png.PROPOSED.jxl look good? [y/N] Does statler_waldorf_cutout.png.PROPOSED.jxl look good? [y/N] @@ -233,14 +246,17 @@ AVIF] (YCbCr) Does wolf.jpg.PROPOSED.webp look good? [y/N] Does wolf.jpg.PROPOSED.webp look good? [y/N] y Success: Created wolf.jpg.webp with quality 83. (Saved 656,433 bytes, 90.80%.) -[AVIF] (YCbCr) +[AVIF] Does wolf.jpg.PROPOSED.avif look good? [y/N] Does wolf.jpg.PROPOSED.avif look good? [y/N] y Does wolf.jpg.PROPOSED.avif look good? [y/N] + Does wolf.jpg.PROPOSED.avif look good? [y/N] Does wolf.jpg.PROPOSED.avif look good? [y/N] y - Does wolf.jpg.PROPOSED.avif look good? [y/N] y - Does wolf.jpg.PROPOSED.avif look good? [y/N] y - Success: Created wolf.jpg.avif with quantizer 23. (Saved 662,814 bytes, 91.68%.) + Does wolf.jpg.PROPOSED.avif look good? [y/N] + Does wolf.jpg.PROPOSED.avif look good? [y/N] + Does wolf.jpg.PROPOSED.avif look good? [y/N] + Does wolf.jpg.PROPOSED.avif look good? [y/N] + Success: Created wolf.jpg.avif with quantizer 18. (Saved 647,124 bytes, 89.51%.) [JPEG XL] Does wolf.jpg.PROPOSED.jxl look good? [y/N] Does wolf.jpg.PROPOSED.jxl look good? [y/N] From bd505b9108bc5b66d3449ab68a45c532e70bd9a7 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Wed, 5 May 2021 08:24:03 -0700 Subject: [PATCH 2/5] misc: add YCbCr short circuit --- refract/Cargo.toml | 4 ++++ refract/src/image.rs | 6 ++++-- refract/src/main.rs | 12 +++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/refract/Cargo.toml b/refract/Cargo.toml index 8129056..61baa26 100644 --- a/refract/Cargo.toml +++ b/refract/Cargo.toml @@ -48,6 +48,10 @@ description = "Skip JPEG XL conversion." long = "--no-webp" description = "Skip WebP conversion." +[[package.metadata.bashman.switches]] +long = "--skip-ycbcr" +description = "Only test full-range RGB AVIF encoding (when encoding AVIFs)." + [[package.metadata.bashman.switches]] short = "-V" long = "--version" diff --git a/refract/src/image.rs b/refract/src/image.rs index 4069236..d9515e5 100644 --- a/refract/src/image.rs +++ b/refract/src/image.rs @@ -37,6 +37,7 @@ pub(super) struct ImageCli<'a> { kind: OutputKind, tmp: PathBuf, dst: PathBuf, + flags: u8, } impl<'a> Drop for ImageCli<'a> { @@ -50,7 +51,7 @@ impl<'a> Drop for ImageCli<'a> { impl<'a> ImageCli<'a> { /// # New Instance. - pub(crate) fn new(src: &'a Source, kind: OutputKind) -> Self { + pub(crate) fn new(src: &'a Source, kind: OutputKind, flags: u8) -> Self { // Let's start by setting up the file system paths we'll be using for // preview and permanent output. let stub: &[u8] = src.path().as_os_str().as_bytes(); @@ -68,6 +69,7 @@ impl<'a> ImageCli<'a> { kind, tmp, dst, + flags, } } @@ -85,7 +87,7 @@ impl<'a> ImageCli<'a> { .with_indent(1); // Loop it. - let mut guide = self.src.encode(self.kind, 0); + let mut guide = self.src.encode(self.kind, self.flags); while guide.advance() .and_then(|data| save_image(&self.tmp, data).ok()) .is_some() diff --git a/refract/src/main.rs b/refract/src/main.rs index 89d1ed5..e9235d9 100644 --- a/refract/src/main.rs +++ b/refract/src/main.rs @@ -37,6 +37,7 @@ use argyle::{ }; use image::ImageCli; use refract_core::{ + FLAG_NO_AVIF_LIMITED, OutputKind, RefractError, Source, @@ -86,6 +87,9 @@ fn _main() -> Result<(), RefractError> { .map_err(RefractError::Menu)? .with_list(); + // We'll get to these in a bit. + let mut flags: u8 = 0; + // Figure out which types we're dealing with. let mut encoders: Vec = Vec::with_capacity(2); if ! args.switch(b"--no-webp") { @@ -93,6 +97,10 @@ fn _main() -> Result<(), RefractError> { } if ! args.switch(b"--no-avif") { encoders.push(OutputKind::Avif); + + if args.switch(b"--skip-ycbcr") { + flags |= FLAG_NO_AVIF_LIMITED; + } } if ! args.switch(b"--no-jxl") { encoders.push(OutputKind::Jxl); @@ -124,7 +132,7 @@ fn _main() -> Result<(), RefractError> { match Source::try_from(x) { Ok(img) => encoders.iter() - .map(|&e| ImageCli::new(&img, e)) + .map(|&e| ImageCli::new(&img, e, flags)) .for_each(ImageCli::encode), Err(e) => Msg::error(e.as_str()).print(), } @@ -172,6 +180,8 @@ FLAGS: --no-avif Skip AVIF conversion. --no-jxl Skip JPEG XL conversion. --no-webp Skip WebP conversion. + --skip-ycbcr Only test full-range RGB AVIF encoding (when encoding + AVIFs). -V, --version Prints version information. OPTIONS: From 9280e3bd9599d06c3c5ea1812211e1c0cd9577b4 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Wed, 5 May 2021 08:24:14 -0700 Subject: [PATCH 3/5] bump: 0.4.1 --- refract/Cargo.toml | 2 +- refract_core/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/refract/Cargo.toml b/refract/Cargo.toml index 61baa26..e290fd9 100644 --- a/refract/Cargo.toml +++ b/refract/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "refract" -version = "0.4.0" +version = "0.4.1" license = "WTFPL" authors = ["Josh Stoik "] edition = "2018" diff --git a/refract_core/Cargo.toml b/refract_core/Cargo.toml index 3227040..df4f99a 100644 --- a/refract_core/Cargo.toml +++ b/refract_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "refract_core" -version = "0.4.0" +version = "0.4.1" license = "WTFPL" authors = ["Josh Stoik "] edition = "2018" From a78c5f08f1de85577e6143ac114448409888cce3 Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Wed, 5 May 2021 08:29:27 -0700 Subject: [PATCH 4/5] docs --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba49293..1c2700a 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,9 @@ The latter is compensated for by automatically repeating the chosen "best" encod Color sources are outputted using `Y′UV444`, while greyscale sources are outputted using `Y′UV400` instead. -Speaking of color sources, Refract will first attempt encoding using limited-range YCbCr as that usually reduces output sizes 2-5%. Because YCbCr can result in color shifting or other undesired distortion, if none of the candidates look good, it will rerun the process using full-range RGB. +Speaking of color sources, Refract attempts AVIF encoding using both limited-range YCbCr and full-range RGB methods. YCbCr typically results in slightly smaller output but may lead to more noticeable color shifts. If you want to skip this, use the `--skip-ycbcr` flag. -Because YCbCr particularly messes with blacks and whites, greyscale images are only ever encoded in full-range mode. +Grescale sources are only ever attempted using full-range RGB. **Note:** >The upcoming release of Chrome v.91 is introducing stricter requirements for AVIF images that will [prevent the rendering of many previously valid sources](https://bugs.chromium.org/p/chromium/issues/detail?id=1115483). This will break a fuckton of images, including those created with Refract < `0.3.1`. Be sure to regenerate any such images using `0.3.1+` to avoid any sadness. @@ -84,6 +84,7 @@ The following flags are available: --no-avif Skip AVIF conversion. --no-jxl Skip JPEG XL conversion. --no-webp Skip WebP conversion. + --skip-ycbcr Only test full-range RGB AVIF encoding (when encoding AVIFs). -V, --version Prints version information. ``` From 33b650395e83af7c959f439326ecd9c225c9bf4b Mon Sep 17 00:00:00 2001 From: Josh Stoik Date: Wed, 5 May 2021 08:36:13 -0700 Subject: [PATCH 5/5] build: 0.4.1 --- CREDITS.md | 4 ++-- release/completions/refract.bash | 1 + release/man/refract.1 | 7 +++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CREDITS.md b/CREDITS.md index b3b79eb..4c938c2 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -2553,8 +2553,8 @@ SOFTWARE. * [dactyl 0.1.7](https://github.com/Blobfolio/dactyl) * [dowser 0.2.2](https://github.com/Blobfolio/dowser) * [fyi_msg 0.7.1](https://github.com/Blobfolio/fyi) -* [refract 0.4.0](https://github.com/Blobfolio/refract) -* [refract_core 0.4.0](https://github.com/Blobfolio/refract) +* [refract 0.4.1](https://github.com/Blobfolio/refract) +* [refract_core 0.4.1](https://github.com/Blobfolio/refract) ``` diff --git a/release/completions/refract.bash b/release/completions/refract.bash index 3953fe4..be23d33 100644 --- a/release/completions/refract.bash +++ b/release/completions/refract.bash @@ -12,6 +12,7 @@ _basher___refract() { [[ " ${COMP_LINE} " =~ " --no-avif " ]] || opts+=("--no-avif") [[ " ${COMP_LINE} " =~ " --no-jxl " ]] || opts+=("--no-jxl") [[ " ${COMP_LINE} " =~ " --no-webp " ]] || opts+=("--no-webp") + [[ " ${COMP_LINE} " =~ " --skip-ycbcr " ]] || opts+=("--skip-ycbcr") if [[ ! " ${COMP_LINE} " =~ " -V " ]] && [[ ! " ${COMP_LINE} " =~ " --version " ]]; then opts+=("-V") opts+=("--version") diff --git a/release/man/refract.1 b/release/man/refract.1 index dc223a9..b609439 100644 --- a/release/man/refract.1 +++ b/release/man/refract.1 @@ -1,6 +1,6 @@ -.TH "REFRACT" "1" "May 2021" "Refract v0.4.0" "User Commands" +.TH "REFRACT" "1" "May 2021" "Refract v0.4.1" "User Commands" .SH NAME -Refract \- Manual page for refract v0.4.0. +Refract \- Manual page for refract v0.4.1. .SH DESCRIPTION Refract is a guided AVIF/JPEG XL/WebP conversion utility for JPEG and PNG sources. .SS USAGE: @@ -20,6 +20,9 @@ Skip JPEG XL conversion. \fB\-\-no\-webp\fR Skip WebP conversion. .TP +\fB\-\-skip\-ycbcr\fR +Only test full\-range RGB AVIF encoding (when encoding AVIFs). +.TP \fB\-V\fR, \fB\-\-version\fR Print program version. .SS OPTIONS: