Skip to content

Commit

Permalink
release: 0.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
joshstoik1 committed May 5, 2021
2 parents c1d83d6 + 8854b1c commit 5b28212
Show file tree
Hide file tree
Showing 19 changed files with 461 additions and 116 deletions.
8 changes: 4 additions & 4 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ limitations under the License.
* [rayon 1.5.0](https://github.com/rayon-rs/rayon)
* [rayon-core 1.9.0](https://github.com/rayon-rs/rayon)
* [scopeguard 1.1.0](https://github.com/bluss/scopeguard)
* [syn 1.0.71](https://github.com/dtolnay/syn)
* [syn 1.0.72](https://github.com/dtolnay/syn)
* [tempfile 3.2.0](https://github.com/Stebalien/tempfile)
* [thiserror 1.0.24](https://github.com/dtolnay/thiserror)
* [thiserror-impl 1.0.24](https://github.com/dtolnay/thiserror)
Expand Down Expand Up @@ -1876,7 +1876,7 @@ limitations under the License.

**Used By:**

* [libavif-sys 0.9.0](https://github.com/njaard/libavif-rs)
* [libavif-sys 0.10.0](https://github.com/njaard/libavif-rs)


```
Expand Down Expand Up @@ -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.3.5](https://github.com/Blobfolio/refract)
* [refract_core 0.3.5](https://github.com/Blobfolio/refract)
* [refract 0.4.0](https://github.com/Blobfolio/refract)
* [refract_core 0.4.0](https://github.com/Blobfolio/refract)


```
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Refract implements [`libavif`](https://github.com/AOMediaCodec/libavif), [`libjx

| Encoding | Mode | Parallel | Comparable To Running |
| -------- | ---- | -------- | --------------------- |
| AVIF | Lossy | Y | `cavif --speed 1 --quality <N>` |
| AVIF | Lossy | Y | `cavif --color rgb --speed 1 --quality <N>` |
| JPEG XL | Lossless | Y | `cjxl --speed tortoise --distance 0.0` |
| JPEG XL | Lossy | Y | `cjxl --speed tortoise --distance <N>` |
| WebP | Lossless | N | `cwebp -lossless -z 9 -q 100` |
Expand All @@ -54,12 +54,17 @@ The guided feedback is only required for lossy stages. Lossless, being lossless,

AVIF encoding is _slow_.

To make it at all bearable, three concessions are made:
* Lossless encoding is unsupported (it isn't competitive anyway);
To make it at all bearable, two concessions are made:
* The encoder is run with speed `1` rather than speed `0`;
* Images are split into "tiles" that can be processed in parallel;

That last item is compensated for by automatically repeating the chosen "best" encoding one time at the end with tiling optimizations disabled.
The latter is compensated for by automatically repeating the chosen "best" encoding one time at the end with tiling optimizations disabled.

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.

Because YCbCr particularly messes with blacks and whites, greyscale images are only ever encoded in full-range mode.

**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.
Expand Down
2 changes: 1 addition & 1 deletion refract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "refract"
version = "0.3.5"
version = "0.4.0"
license = "WTFPL"
authors = ["Josh Stoik <[email protected]>"]
edition = "2018"
Expand Down
55 changes: 46 additions & 9 deletions refract/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use dactyl::{
};
use fyi_msg::Msg;
use refract_core::{
FLAG_AVIF_LIMITED,
Output,
OutputKind,
RefractError,
Expand Down Expand Up @@ -37,6 +38,7 @@ pub(super) struct ImageCli<'a> {
kind: OutputKind,
tmp: PathBuf,
dst: PathBuf,
flags: u8,
}

impl<'a> Drop for ImageCli<'a> {
Expand All @@ -63,18 +65,35 @@ 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(self) {
pub(crate) fn encode(mut self) {
// Print a header for the encoding type.
println!("\x1b[34m[\x1b[96;1m{}\x1b[0;34m]\x1b[0m", self.kind);
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 { "" }
);

// We'll be re-using this prompt throughout.
let prompt = Msg::plain(format!(
Expand All @@ -85,9 +104,8 @@ impl<'a> ImageCli<'a> {
.with_indent(1);

// Loop it.
let mut guide = self.src.encode(self.kind);
while guide.next()
.and_then(|_| guide.candidate())
let mut guide = self.src.encode(self.kind, self.flags);
while guide.advance()
.and_then(|data| save_image(&self.tmp, data).ok())
.is_some()
{
Expand All @@ -101,7 +119,7 @@ impl<'a> ImageCli<'a> {

// Wrap it up!
let time = guide.time();
self.finish(guide.take());
let res = self.finish(guide.take());

// Print the timings.
Msg::plain(format!(
Expand All @@ -110,10 +128,21 @@ 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<Output, RefractError>) {
fn finish(&self, result: Result<Output, RefractError>) -> bool {
// Handle results.
match result {
Ok(result) => match save_image(&self.dst, &result) {
Expand Down Expand Up @@ -154,14 +183,20 @@ pub(super) fn print_path_title(path: &Path) {
}

/// # Print Error.
fn print_error(err: RefractError) {
///
/// This always returns false.
fn print_error(err: RefractError) -> bool {
Msg::warning(err.as_str())
.with_indent(1)
.print();

false
}

/// # Print Success.
fn print_success(src_size: u64, output: &Output, dst_path: &Path) {
///
/// This always returns true.
fn print_success(src_size: u64, output: &Output, dst_path: &Path) -> bool {
let diff: u64 = src_size - output.size().get();
let per = dactyl::int_div_float(diff, src_size);
let name = dst_path.file_name()
Expand Down Expand Up @@ -189,6 +224,8 @@ fn print_success(src_size: u64, output: &Output, dst_path: &Path) {
}
)
.print();

true
}

/// # Write Result.
Expand Down
4 changes: 2 additions & 2 deletions refract_core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "refract_core"
version = "0.3.5"
version = "0.4.0"
license = "WTFPL"
authors = ["Josh Stoik <[email protected]>"]
edition = "2018"
Expand Down Expand Up @@ -28,7 +28,7 @@ default-features = false
features = [ "threads" ]

[dependencies.libavif-sys]
version = "0.9.0"
version = "0.10.0"
default-features = false
features = [ "codec-rav1e" ]

Expand Down
75 changes: 47 additions & 28 deletions refract_core/src/enc/avif.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,25 @@ use crate::{
RefractError,
};
use libavif_sys::{
AVIF_CHROMA_UPSAMPLING_BILINEAR,
AVIF_CHROMA_SAMPLE_POSITION_COLOCATED,
AVIF_CODEC_CHOICE_RAV1E,
AVIF_PLANES_YUV,
AVIF_COLOR_PRIMARIES_BT709,
AVIF_MATRIX_COEFFICIENTS_BT709,
AVIF_MATRIX_COEFFICIENTS_IDENTITY,
AVIF_PIXEL_FORMAT_YUV400,
AVIF_PIXEL_FORMAT_YUV444,
AVIF_RANGE_FULL,
AVIF_RANGE_LIMITED,
AVIF_RESULT_OK,
AVIF_RGB_FORMAT_RGBA,
AVIF_TRANSFER_CHARACTERISTICS_SRGB,
avifEncoder,
avifEncoderCreate,
avifEncoderDestroy,
avifEncoderWrite,
avifImage,
avifImageAllocatePlanes,
avifImageCreate,
avifImageDestroy,
avifImageRGBToYUV,
avifResult,
avifRGBImage,
avifRWData,
avifRWDataFree,
};
Expand Down Expand Up @@ -64,6 +67,8 @@ impl TryFrom<NonZeroU8> for LibAvifEncoder {

// Start up the encoder!
let encoder = unsafe { avifEncoderCreate() };
if encoder.is_null() { return Err(RefractError::Encode); }

unsafe {
(*encoder).codecChoice = AVIF_CODEC_CHOICE_RAV1E;
(*encoder).maxThreads = threads;
Expand Down Expand Up @@ -92,44 +97,58 @@ impl Drop for LibAvifEncoder {

/// # Avif Image.
///
/// This holds a YUV copy of the image, which is created in a roundabout way
/// by converting a raw slice into an RGB image. Haha.
///
/// The struct includes initialization helpers, but exists primarily for
/// garbage cleanup.
struct LibAvifImage(*mut avifImage);

impl TryFrom<&Image<'_>> for LibAvifImage {
type Error = RefractError;

#[allow(clippy::cast_possible_truncation)] // The values are purpose-made.
fn try_from(src: &Image) -> Result<Self, Self::Error> {
// 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);
}

// 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,
};
let limited = src.is_yuv_limited();
let greyscale: bool = src.color_kind().is_greyscale();

// And convert it to YUV.
let yuv = unsafe {
let tmp = avifImageCreate(
src.width_i32()?,
src.height_i32()?,
8, // Depth.
1, // YUV444 = 1_i32
if greyscale { AVIF_PIXEL_FORMAT_YUV400 }
else { AVIF_PIXEL_FORMAT_YUV444 }
);
avifImageAllocatePlanes(tmp, AVIF_PLANES_YUV as _);
maybe_die(avifImageRGBToYUV(tmp, &rgb))?;

// 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 _;
(*tmp).transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB as _;
(*tmp).matrixCoefficients =
if greyscale || limited { AVIF_MATRIX_COEFFICIENTS_BT709 as _ }
else { AVIF_MATRIX_COEFFICIENTS_IDENTITY as _ };

tmp
};

Expand Down
Loading

0 comments on commit 5b28212

Please sign in to comment.