Lossy WebP: a pure-Rust VP8 encoder with quality and lossless controls (#648) (#880)

* Lossy WebP: a pure-Rust VP8 encoder with quality and lossless controls (#648)

crates/codecs gains codecs/vp8: a clean-room VP8 key-frame encoder from RFC 6386
(boolean coder, 16x16 and 4x4 intra prediction with per-macroblock mode decision,
forward DCT/WHT paired with the bit-exact inverse transforms, dc/ac quantiser tables,
token coding with per-frame probability updates, skipped macroblocks). webp.rs wraps
the frame in the RIFF container (bare VP8, or VP8X with ALPH alpha and ICCP/EXIF/XMP).
EncodeOptions: webp_lossless now picks the encoder instead of erroring, webp_quality
(0-100) sets the quantiser; a lossy WebP reports a LossyCompression fidelity warning.

Export As: the WebP entry gets Quality and a Lossless switch (new tl! string in all
catalogs). CLI --quality, MCP/RPC doc_save quality and the engine's JPEG-style
quality turn a WebP lossy; without a quality WebP stays lossless as before.

Tests: tests/webp_lossy.rs decodes every file with image-webp (PSNR by quality,
sizes fall with quality and are >= 4x below lossless on noisy photos, flat frames
under 1.2 KB, alpha exact, metadata round trip, odd/tiny sizes, 16-bit input, the
16383 px limit as an error, text edges), plus unit tests for the coder, transforms,
prediction and header.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* i18n(it): Lossless (Export As WebP switch) in the Italian catalog that landed on main

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

---------

Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
ljkx 2026-10-08 03:12:38 +02:00 committed by GitHub
parent fdd7141788
commit 735bb09a98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 2603 additions and 22 deletions

View File

@ -198,7 +198,7 @@ Every screenshot here is the real app at work on public-domain art, rendered off
</td>
<td width="33%" valign="top">
<h4>🗂️ Formats</h4>
PSD and PSB, layered TIFF (Photoshop's layer data in the TIFF, read and written in either byte order), plus flat PNG, JPEG, TIFF, WebP, GIF, BMP, TGA, ICO, QOI, PNM, OpenEXR, Radiance HDR and AVIF, with symmetric read and write at 8, 16 and 32 bits, HEIC photos from iPhone and Mac (read; in official builds, an optional <code>--features heif</code> build feature), and the native <code>.pcraft</code> format.
PSD and PSB, layered TIFF (Photoshop's layer data in the TIFF, read and written in either byte order), plus flat PNG, JPEG, TIFF, WebP (lossy and lossless), GIF, BMP, TGA, ICO, QOI, PNM, OpenEXR, Radiance HDR and AVIF, with symmetric read and write at 8, 16 and 32 bits, HEIC photos from iPhone and Mac (read; in official builds, an optional <code>--features heif</code> build feature), and the native <code>.pcraft</code> format.
</td>
<td width="33%" valign="top">
<h4>🪄 The everyday essentials</h4>

View File

@ -17,6 +17,7 @@ USAGE:
photocraft-cli convert <in> <out> [--format <ext>] [--quality <1-100>] [--tiff-layers]
Convert between formats (.pcraft, .psd, .png, .jpg, .tif, .webp, .exr, …).
TIFF output is flat unless --tiff-layers keeps the layers (Photoshop layer data).
--quality sets the JPEG or WebP quality; a WebP written with a quality is lossy, without one lossless.
photocraft-cli info <file> [--compact]
Print the document as JSON (size, mode, depth, layer tree).
photocraft-cli run (<file> | --new <json>) --cmd <id> [--params <json>] [--cmd …] [--out <file>] [--format <ext>] [--quality <1-100>] [--tiff-layers]
@ -170,8 +171,11 @@ fn export_opts(a: &Args) -> Result<ExportOptions, String> {
// TIFF output is flat unless asked: scripted conversions keep predictable sizes.
let mut o = ExportOptions { tiff_layers: a.has("--tiff-layers"), ..Default::default() };
if let Some(q) = a.get("--quality") {
o.encode.jpeg_quality =
q.parse().ok().filter(|q| (1..=100).contains(q)).ok_or_else(|| format!("bad --quality `{q}`: expected a whole number from 1 to 100"))?;
let q = q.parse().ok().filter(|q| (1..=100).contains(q)).ok_or_else(|| format!("bad --quality `{q}`: expected a whole number from 1 to 100"))?;
o.encode.jpeg_quality = q;
// Asking for a quality asks for a lossy WebP; the default WebP stays lossless.
o.encode.webp_quality = q;
o.encode.webp_lossless = false;
}
Ok(o)
}

View File

@ -144,6 +144,10 @@ fn services(inbox: Inbox, ctx: egui::Context) -> Services {
if let Some(q) = settings.jpeg_quality {
opts.encode.jpeg_quality = q;
}
opts.encode.webp_lossless = settings.webp_lossless;
if let Some(q) = settings.webp_quality {
opts.encode.webp_quality = q;
}
opts.tiff_layers = settings.tiff_layers;
opts.xmp = if settings.xmp_all { photocraft_io::XmpEmbed::All } else { photocraft_io::XmpEmbed::None };
photocraft_io::export(doc, path, &opts).map(|r| (r.bytes, r.warnings)).map_err(|e| e.to_string())

View File

@ -155,6 +155,10 @@ pub fn native(automation: Option<photocraft_automation::AuthorizedWorkspace>) ->
if let Some(q) = settings.jpeg_quality {
opts.encode.jpeg_quality = q;
}
opts.encode.webp_lossless = settings.webp_lossless;
if let Some(q) = settings.webp_quality {
opts.encode.webp_quality = q;
}
opts.tiff_layers = settings.tiff_layers;
opts.xmp = if settings.xmp_all { photocraft_io::XmpEmbed::All } else { photocraft_io::XmpEmbed::None };
crate::crash_guard::guard("Export", || photocraft_io::export(doc, path, &opts).map(|r| (r.bytes, r.warnings)).map_err(|e| e.to_string()))

View File

@ -6,6 +6,10 @@
A TIFF saved by Photoshop with its layers carries them in two private tags: 37724 (`ImageSourceData`, the same layer section a PSD has, preceded by the string `Adobe Photoshop Document Data Block`) and 34377 (the PSD image resources). `photocraft-codecs` passes both through as opaque bytes on `Metadata`; `photocraft-io` rebuilds a PSD model from them plus the TIFF's pixels and hands it to the PSD importer, so groups, masks, layer styles, type, shapes, smart objects and adjustment layers open from a TIFF exactly as from a PSD, and are written back the same way. In Intel-order (`II`) files Photoshop stores the whole block structure byte-swapped (reversed keys such as `MIB8` and `ryaL`, little-endian integers and doubles) while pixel samples stay big-endian; `photocraft-psd`'s `tiff` module converts between the two orders field by field, and drops, with a warning, any block whose layout it does not know rather than pass byte-swapped data to other readers. Limits: a TIFF holds one transparency sample, so extra alpha channels are not written; Lab documents are saved flat. Save As asks "Including layers will increase file size" (with "Discard Layers and Save a Copy") while *Ask Before Saving Layered TIFF Files* is on in Preferences › File Handling. Scripted and agent saves write a flat TIFF unless they ask for layers: `--tiff-layers` for `photocraft-cli convert`, `run` and `batch`, and `"tiffLayers": true` in the save, batch and export commands and MCP `doc_save`.
## WebP
Lossless WebP (VP8L) is encoded and decoded by `image-webp`. Lossy WebP is written by PhotoCraft's own VP8 key-frame encoder in `crates/codecs/src/codecs/vp8`, a clean-room implementation of RFC 6386: BT.601 4:2:0 conversion, 16×16 and 4×4 intra prediction chosen per macroblock by reconstruction error, the forward DCT and Walsh-Hadamard transforms paired with the bit-exact inverse transforms, quantisation on the RFC's `dc_qlookup`/`ac_qlookup` tables, token coding with per-frame probability updates, and skipped macroblocks for flat areas. `EncodeOptions::webp_lossless` picks the encoder and `webp_quality` (0–100, the libwebp scale) sets the quantiser; alpha travels losslessly in an `ALPH` chunk and ICC/EXIF/XMP in their own chunks of the extended (`VP8X`) container. The decoder applies the frame's loop filter; no C library is involved on either side.
Capability details such as sample depth, channel layout, alpha, ICC, EXIF, XMP, DPI, and animation behavior are defined by `crates/codecs/src/format.rs`. Do not infer fidelity from the filename extension alone.
## Exporting documents

View File

@ -101,6 +101,8 @@ impl Headless {
photocraft_io::ExportOptions { tiff_layers: p.get("tiffLayers").and_then(Value::as_bool).unwrap_or(false), ..Default::default() };
if let Some(q) = p.get("quality").and_then(Value::as_u64) {
opts.encode.jpeg_quality = q.clamp(1, 100) as u8;
opts.encode.webp_quality = q.clamp(1, 100) as u8;
opts.encode.webp_lossless = false;
}
let path = str_of(&p, "path").map(PathBuf::from);
self.save(index_of(&p), path.as_deref(), str_of(&p, "format"), &opts)

View File

@ -80,7 +80,7 @@ pub struct SaveParams {
/// Format override as an extension (pcraft, psd, png, jpg, tif, webp, exr, …).
#[serde(default)]
pub format: Option<String>,
/// JPEG quality 1..100 for lossy exports.
/// JPEG or WebP quality 1..100. A WebP saved with a quality is lossy; without one it is lossless.
#[serde(default)]
pub quality: Option<u8>,
/// TIFF: keep the layers (Photoshop layer data). Off by default: a flat TIFF.
@ -663,6 +663,8 @@ impl PhotocraftMcp {
let mut opts = photocraft_io::ExportOptions { tiff_layers: p.tiff_layers, ..Default::default() };
if let Some(q) = p.quality {
opts.encode.jpeg_quality = q.clamp(1, 100);
opts.encode.webp_quality = q.clamp(1, 100);
opts.encode.webp_lossless = false;
}
let path = p.path.map(PathBuf::from);
h.save(p.index, path.as_deref(), p.format.as_deref(), &opts)

View File

@ -6,4 +6,5 @@ pub(crate) mod png;
pub(crate) mod pnm;
pub(crate) mod tiff;
pub(crate) mod via_image;
pub(crate) mod vp8;
pub(crate) mod webp;

View File

@ -0,0 +1,273 @@
//! The VP8 boolean entropy coder, writing side (RFC 6386 §7): every symbol is a bool with an
//! 8-bit probability of being zero; literals and tree-coded values are built from bools.
use super::tables::Tree;
/// Writes bools at given probabilities into a byte buffer.
pub struct BoolEncoder {
out: Vec<u8>,
range: u32,
bottom: u32,
bit_count: i32,
}
impl Default for BoolEncoder {
fn default() -> Self {
Self::new()
}
}
impl BoolEncoder {
pub fn new() -> Self {
BoolEncoder { out: Vec::new(), range: 255, bottom: 0, bit_count: 24 }
}
/// Propagates a carry into the bytes already written (never past the start: the coded
/// value is below one).
fn add_one_to_output(&mut self) {
for b in self.out.iter_mut().rev() {
if *b == 255 {
*b = 0;
} else {
*b += 1;
return;
}
}
}
/// Writes `bit` whose probability of being 0 is `prob / 256`.
pub fn put(&mut self, prob: u8, bit: bool) {
let split = 1 + (((self.range - 1) * u32::from(prob)) >> 8);
if bit {
self.bottom = self.bottom.wrapping_add(split);
self.range -= split;
} else {
self.range = split;
}
while self.range < 128 {
self.range <<= 1;
if self.bottom & (1 << 31) != 0 {
self.add_one_to_output();
}
self.bottom <<= 1;
self.bit_count -= 1;
if self.bit_count == 0 {
self.out.push((self.bottom >> 24) as u8);
self.bottom &= (1 << 24) - 1;
self.bit_count = 8;
}
}
}
/// A bool at probability 1/2.
pub fn put_flag(&mut self, bit: bool) {
self.put(128, bit);
}
/// An `n`-bit unsigned literal, most significant bit first, each bit at probability 1/2.
pub fn put_literal(&mut self, n: u32, value: u32) {
for i in (0..n).rev() {
self.put_flag((value >> i) & 1 != 0);
}
}
/// A tree-coded value: the path of bools from the root to the leaf `value`, each node's bool
/// at `probs[node / 2]`.
pub fn put_tree(&mut self, tree: Tree, probs: &[u8], value: u8) {
self.put_tree_from(tree, probs, value, 0);
}
/// [`Self::put_tree`] starting at node `start` (2 skips the first branch of the coefficient tree).
pub fn put_tree_from(&mut self, tree: Tree, probs: &[u8], value: u8, start: usize) {
// Find the path to the leaf by searching the tree; trees are tiny (≤ 22 entries).
let mut path = Vec::with_capacity(8);
if !Self::find(tree, start, value, &mut path) {
return;
}
for (node, bit) in path {
self.put(probs.get(node >> 1).copied().unwrap_or(128), bit);
}
}
fn find(tree: Tree, node: usize, value: u8, path: &mut Vec<(usize, bool)>) -> bool {
for bit in [false, true] {
let i = node + usize::from(bit);
let Some(&entry) = tree.get(i) else { continue };
path.push((node, bit));
if entry <= 0 {
if (-entry) as u8 == value {
return true;
}
} else if Self::find(tree, entry as usize, value, path) {
return true;
}
path.pop();
}
false
}
/// Finishes the partition and returns its bytes.
pub fn finish(mut self) -> Vec<u8> {
let mut c = self.bit_count;
let mut v = self.bottom;
if c <= 32 && v & (1u32 << (32 - c)) != 0 {
self.add_one_to_output();
}
v <<= (c & 7) as u32;
c >>= 3;
while c > 0 {
v <<= 8;
c -= 1;
}
for _ in 0..4 {
self.out.push((v >> 24) as u8);
v <<= 8;
}
self.out
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The decoder from RFC 6386 §7.3, for the round-trip tests.
struct BoolDecoder<'a> {
input: &'a [u8],
pos: usize,
range: u32,
value: u32,
bit_count: i32,
}
impl<'a> BoolDecoder<'a> {
fn new(input: &'a [u8]) -> Self {
let mut value = 0;
for i in 0..2 {
value = (value << 8) | u32::from(input.get(i).copied().unwrap_or(0));
}
BoolDecoder { input, pos: 2, range: 255, value, bit_count: 0 }
}
fn get(&mut self, prob: u8) -> bool {
let split = 1 + (((self.range - 1) * u32::from(prob)) >> 8);
let big_split = split << 8;
let bit = if self.value >= big_split {
self.range -= split;
self.value -= big_split;
true
} else {
self.range = split;
false
};
while self.range < 128 {
self.value <<= 1;
self.range <<= 1;
self.bit_count += 1;
if self.bit_count == 8 {
self.bit_count = 0;
self.value |= u32::from(self.input.get(self.pos).copied().unwrap_or(0));
self.pos += 1;
}
}
bit
}
fn literal(&mut self, n: u32) -> u32 {
(0..n).fold(0, |v, _| (v << 1) | u32::from(self.get(128)))
}
fn tree(&mut self, tree: Tree, probs: &[u8], start: usize) -> u8 {
let mut i = start;
loop {
let bit = self.get(probs[i >> 1]);
let entry = tree[i + usize::from(bit)];
if entry <= 0 {
return (-entry) as u8;
}
i = entry as usize;
}
}
}
#[test]
fn bools_round_trip_at_every_probability() {
let mut rng = 0x1234_5678_9abc_def0u64;
let mut next = || {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
rng
};
let symbols: Vec<(u8, bool)> = (0..20_000)
.map(|_| {
let r = next();
let prob = (r & 0xff) as u8;
// Bits follow their probability, roughly, so both paths are exercised.
((prob.max(1)), (r >> 8) & 0xff >= u64::from(prob))
})
.collect();
let mut e = BoolEncoder::new();
for &(p, b) in &symbols {
e.put(p, b);
}
let bytes = e.finish();
let mut d = BoolDecoder::new(&bytes);
for &(p, b) in &symbols {
assert_eq!(d.get(p), b);
}
}
#[test]
fn literals_and_trees_round_trip() {
use super::super::tables::*;
let mut e = BoolEncoder::new();
e.put_literal(7, 93);
e.put_literal(19, 0x7_ffff);
e.put_literal(1, 0);
e.put_tree(KF_YMODE_TREE, &KF_YMODE_PROB, TM_PRED);
e.put_tree(KF_YMODE_TREE, &KF_YMODE_PROB, B_PRED);
e.put_tree(UV_MODE_TREE, &KF_UV_MODE_PROB, H_PRED);
for m in 0..10u8 {
e.put_tree(BMODE_TREE, &KF_BMODE_PROBS[3][7], m);
}
let probs = [200u8, 150, 100, 128, 128, 128, 128, 128, 128, 128, 128];
for t in 0..12u8 {
e.put_tree(COEFF_TREE, &probs, t);
}
// From node 2 (after a zero token) the end-of-block leaf is unreachable: tokens 0..=10.
for t in 0..11u8 {
e.put_tree_from(COEFF_TREE, &probs, t, 2);
}
let bytes = e.finish();
let mut d = BoolDecoder::new(&bytes);
assert_eq!(d.literal(7), 93);
assert_eq!(d.literal(19), 0x7_ffff);
assert_eq!(d.literal(1), 0);
assert_eq!(d.tree(KF_YMODE_TREE, &KF_YMODE_PROB, 0), TM_PRED);
assert_eq!(d.tree(KF_YMODE_TREE, &KF_YMODE_PROB, 0), B_PRED);
assert_eq!(d.tree(UV_MODE_TREE, &KF_UV_MODE_PROB, 0), H_PRED);
for m in 0..10u8 {
assert_eq!(d.tree(BMODE_TREE, &KF_BMODE_PROBS[3][7], 0), m);
}
for t in 0..12u8 {
assert_eq!(d.tree(COEFF_TREE, &probs, 0), t);
}
for t in 0..11u8 {
assert_eq!(d.tree(COEFF_TREE, &probs, 2), t);
}
}
#[test]
fn carry_propagates_through_full_bytes() {
// Many ones at a tiny probability push the interval up and force carries.
let mut e = BoolEncoder::new();
for _ in 0..3000 {
e.put(1, true);
e.put(255, false);
}
let bytes = e.finish();
let mut d = BoolDecoder::new(&bytes);
for _ in 0..3000 {
assert!(d.get(1));
assert!(!d.get(255));
}
}
}

View File

@ -0,0 +1,839 @@
//! A clean-room VP8 key-frame encoder (the lossy WebP bitstream), implemented from RFC 6386.
//!
//! One intra frame: 16×16 macroblocks in raster order, each predicted from the frame's own
//! reconstruction with one of the four whole-block luma modes or the sixteen 4×4 sub-block
//! modes (`B_PRED`), chroma with one of four modes; the residual goes through the 4×4 DCT
//! (and the WHT of the luma DCs for whole-block modes), one frame-wide quantizer derived from the
//! quality, and the boolean entropy coder with the specification's default token probabilities,
//! refined per frame from the frame's own token statistics. Macroblocks without coefficients
//! are skipped. The loop filter is left to the decoder at a level matched to the quantizer.
//!
//! What it does not do (yet): rate-distortion optimized mode decisions and quantization,
//! segments, multiple token partitions, or SIMD. It is deterministic, allocation-bounded and
//! never panics: dimensions are checked up front and every index is derived from them.
mod bool_encoder;
mod predict;
mod tables;
mod transform;
use bool_encoder::BoolEncoder;
use predict::{Plane, SubEdges, predict_block, predict_sub};
use tables::*;
use transform::{fdct, fwht, idct, iwht};
/// The largest dimension a VP8 frame header can hold (14 bits).
pub const MAX_DIMENSION: u32 = 16383;
/// A planar 4:2:0 picture: `y` is `width × height`, `u` and `v` are `ceil(w/2) × ceil(h/2)`.
pub struct Yuv {
pub width: u32,
pub height: u32,
pub y: Vec<u8>,
pub u: Vec<u8>,
pub v: Vec<u8>,
}
impl Yuv {
/// Converts interleaved 8-bit RGB (`stride` bytes per pixel, 3 or 4) to studio-range BT.601
/// Y'CbCr with the chroma averaged over each 2 × 2 block, the convention WebP decoders invert.
pub fn from_rgb(rgb: &[u8], width: u32, height: u32, stride: usize) -> Yuv {
let (w, h) = (width as usize, height as usize);
let (cw, ch) = (w.div_ceil(2), h.div_ceil(2));
let mut y = vec![0u8; w * h];
let mut u = vec![0u8; cw * ch];
let mut v = vec![0u8; cw * ch];
let px = |x: usize, yy: usize| -> (i32, i32, i32) {
let i = (yy.min(h - 1) * w + x.min(w - 1)) * stride;
(i32::from(rgb[i]), i32::from(rgb[i + 1]), i32::from(rgb[i + 2]))
};
for yy in 0..h {
for x in 0..w {
let (r, g, b) = px(x, yy);
y[yy * w + x] = (((66 * r + 129 * g + 25 * b + 128) >> 8) + 16) as u8;
}
}
for cy in 0..ch {
for cx in 0..cw {
let (mut r, mut g, mut b) = (0, 0, 0);
for (dx, dy) in [(0, 0), (1, 0), (0, 1), (1, 1)] {
let (pr, pg, pb) = px(cx * 2 + dx, cy * 2 + dy);
r += pr;
g += pg;
b += pb;
}
let (r, g, b) = ((r + 2) >> 2, (g + 2) >> 2, (b + 2) >> 2);
u[cy * cw + cx] = (((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128).clamp(0, 255) as u8;
v[cy * cw + cx] = (((112 * r - 94 * g - 18 * b + 128) >> 8) + 128).clamp(0, 255) as u8;
}
}
Yuv { width, height, y, u, v }
}
}
/// Encoder settings.
#[derive(Debug, Clone, Copy)]
pub struct Params {
/// 0 (smallest) … 100 (best); maps to the frame quantizer.
pub quality: u8,
}
/// Why a frame cannot be encoded.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Vp8Error {
/// A dimension is 0 or above [`MAX_DIMENSION`].
Dimensions,
/// The plane buffers do not match the dimensions.
Planes,
}
impl std::fmt::Display for Vp8Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Vp8Error::Dimensions => write!(f, "VP8 frames are 1 to {MAX_DIMENSION} pixels per side"),
Vp8Error::Planes => write!(f, "plane sizes do not match the dimensions"),
}
}
}
/// Quality 0..=100 → quantizer index 0..=127 (0 is finest). A gentle curve: quality 90 ≈ 11,
/// 75 ≈ 28, 50 ≈ 60, 25 ≈ 92.
pub fn quantizer_index(quality: u8) -> u8 {
let q = f64::from(quality.min(100));
let t = (100.0 - q) / 100.0;
(t.powf(1.1) * 127.0).round().clamp(0.0, 127.0) as u8
}
/// Dequantization factors for one quantizer index (RFC 6386 §14.1 and dixie's `dequant_init`).
#[derive(Debug, Clone, Copy)]
struct Quant {
y_dc: i32,
y_ac: i32,
y2_dc: i32,
y2_ac: i32,
uv_dc: i32,
uv_ac: i32,
}
impl Quant {
fn new(qi: u8) -> Quant {
let qi = usize::from(qi.min(127));
Quant {
y_dc: DC_QLOOKUP[qi],
y_ac: AC_QLOOKUP[qi],
y2_dc: DC_QLOOKUP[qi] * 2,
y2_ac: (AC_QLOOKUP[qi] * 155 / 100).max(8),
uv_dc: DC_QLOOKUP[qi].min(132),
uv_ac: AC_QLOOKUP[qi],
}
}
}
/// Quantizes one coefficient: round to nearest for DC, with a small dead zone for AC (fewer
/// ±1 coefficients cost far fewer bits than they are worth in distortion).
#[inline]
fn quantize(coeff: i32, factor: i32, dc: bool) -> i32 {
let bias = if dc { factor / 2 } else { factor * 7 / 16 };
let q = (coeff.abs() + bias) / factor;
let q = q.min(2048);
if coeff < 0 { -q } else { q }
}
/// Block types of the token probability table (RFC 6386 §13.3).
const TYPE_Y_AFTER_Y2: usize = 0;
const TYPE_Y2: usize = 1;
const TYPE_UV: usize = 2;
const TYPE_Y_WITH_DC: usize = 3;
/// Which coefficient token codes `v` and, for the categories, the extra bits to send.
fn token_of(v: i32) -> (u8, Option<(usize, u32)>) {
let a = v.unsigned_abs() as i32;
match a {
0 => (DCT_0, None),
1 => (DCT_1, None),
2 => (DCT_2, None),
3 => (DCT_3, None),
4 => (DCT_4, None),
5..=6 => (DCT_CAT1, Some((0, (a - CAT_BASE[0]) as u32))),
7..=10 => (DCT_CAT2, Some((1, (a - CAT_BASE[1]) as u32))),
11..=18 => (DCT_CAT3, Some((2, (a - CAT_BASE[2]) as u32))),
19..=34 => (DCT_CAT4, Some((3, (a - CAT_BASE[3]) as u32))),
35..=66 => (DCT_CAT5, Some((4, (a - CAT_BASE[4]) as u32))),
_ => (DCT_CAT6, Some((5, (a - CAT_BASE[5]) as u32))),
}
}
/// One coded block's quantized coefficients in zig-zag order, from `first` (1 for Y after Y2).
struct Block {
coeffs: [i32; 16],
first: usize,
/// One past the last non-zero zig-zag position (`first` when the block is empty).
eob: usize,
}
impl Block {
fn from_raster(raster: &[i32; 16], first: usize) -> Block {
let mut coeffs = [0i32; 16];
for (pos, &idx) in ZIGZAG.iter().enumerate() {
coeffs[pos] = raster[idx];
}
let eob = (first..16).rev().find(|&p| coeffs[p] != 0).map_or(first, |p| p + 1);
Block { coeffs, first, eob }
}
fn has_coeffs(&self) -> bool {
self.eob > self.first
}
}
/// Token-count statistics, `[type][band][ctx][token]`, from which the frame's probabilities are
/// derived (and the per-node branch counts needed to turn them into tree probabilities).
#[derive(Clone)]
struct Stats {
/// Per tree node (11 per context): how often the bool was 0 and how often 1.
node: Vec<[[[[u32; 2]; 11]; 3]; 8]>,
}
impl Stats {
fn new() -> Stats {
Stats { node: vec![[[[[0; 2]; 11]; 3]; 8]; 4] }
}
}
/// Everything about a block's tokens the two passes share: the branch path of each token in the
/// coefficient tree. Walked once per block to count branches, once to write them.
fn tree_path(value: u8, start: usize) -> Vec<(usize, bool)> {
fn find(node: usize, value: u8, path: &mut Vec<(usize, bool)>) -> bool {
for bit in [false, true] {
let i = node + usize::from(bit);
let Some(&entry) = COEFF_TREE.get(i) else { continue };
path.push((node >> 1, bit));
if entry <= 0 {
if (-entry) as u8 == value {
return true;
}
} else if find(entry as usize, value, path) {
return true;
}
path.pop();
}
false
}
let mut path = Vec::with_capacity(8);
find(start, value, &mut path);
path
}
/// One bool of a block's token sequence.
enum Event {
/// A coefficient-tree branch: `(type, band, ctx, node, bit)`, coded with `probs[type][band][ctx][node]`.
Node(usize, usize, usize, usize, bool),
/// The extra bits of a category token: `(category, bits)`.
Extra(usize, u32),
/// The sign of a non-zero coefficient (`true` = negative).
Sign(bool),
}
/// Visits every bool of a block's token sequence in coding order.
fn for_each_token_bool(block: &Block, ty: usize, ctx0: usize, mut f: impl FnMut(Event)) {
let mut ctx = ctx0;
let mut prev_zero = false;
let mut pos = block.first;
while pos < block.eob {
let v = block.coeffs[pos];
let (token, cat) = token_of(v);
let band = COEFF_BANDS[pos];
for (node, bit) in tree_path(token, if prev_zero { 2 } else { 0 }) {
f(Event::Node(ty, band, ctx, node, bit));
}
if let Some((c, bits)) = cat {
f(Event::Extra(c, bits));
}
if v != 0 {
f(Event::Sign(v < 0));
}
ctx = match v.abs() {
0 => 0,
1 => 1,
_ => 2,
};
prev_zero = v == 0;
pos += 1;
}
if block.eob < 16 {
// End of block (never directly after a zero: the coded run ends with a non-zero value).
let band = COEFF_BANDS[block.eob];
for (node, bit) in tree_path(DCT_EOB, 0) {
f(Event::Node(ty, band, ctx, node, bit));
}
}
}
/// A macroblock's decisions and coefficients.
struct Macroblock {
y_mode: u8,
b_modes: [u8; 16],
uv_mode: u8,
/// Y2 (whole-block modes only), 16 Y, 4 U, 4 V.
y2: Option<Block>,
y: Vec<Block>,
u: Vec<Block>,
v: Vec<Block>,
skip: bool,
}
/// Non-zero contexts: 4 Y (by column for "above", by row for "left"), 2 U, 2 V, 1 Y2.
type Ctx = [u8; 9];
struct Encoder<'a> {
src: &'a Yuv,
mb_cols: usize,
mb_rows: usize,
q: Quant,
rec_y: Plane,
rec_u: Plane,
rec_v: Plane,
mbs: Vec<Macroblock>,
}
impl<'a> Encoder<'a> {
fn new(src: &'a Yuv, qi: u8) -> Encoder<'a> {
let mb_cols = (src.width as usize).div_ceil(16);
let mb_rows = (src.height as usize).div_ceil(16);
Encoder {
src,
mb_cols,
mb_rows,
q: Quant::new(qi),
rec_y: Plane::new(mb_cols * 16, mb_rows * 16),
rec_u: Plane::new(mb_cols * 8, mb_rows * 8),
rec_v: Plane::new(mb_cols * 8, mb_rows * 8),
mbs: Vec::with_capacity(mb_cols * mb_rows),
}
}
/// Source luma at (`x`, `y`), the frame's last row and column extended over the padding.
#[inline]
fn src_y(&self, x: usize, y: usize) -> i32 {
let (w, h) = (self.src.width as usize, self.src.height as usize);
i32::from(self.src.y[y.min(h - 1) * w + x.min(w - 1)])
}
#[inline]
fn src_c(&self, plane: &[u8], x: usize, y: usize) -> i32 {
let (w, h) = ((self.src.width as usize).div_ceil(2), (self.src.height as usize).div_ceil(2));
i32::from(plane[y.min(h - 1) * w + x.min(w - 1)])
}
/// Encodes one 4×4 block: residual → coefficients → quantized (in raster order, with the
/// quantization factors) → the dequantized coefficients the decoder will see.
fn code_block(residual: &[i32; 16], dc_factor: i32, ac_factor: i32, with_dc: bool) -> ([i32; 16], [i32; 16]) {
let coeffs = fdct(residual);
let mut quantized = [0i32; 16];
let mut dequantized = [0i32; 16];
for i in 0..16 {
let factor = if i == 0 { dc_factor } else { ac_factor };
if i == 0 && !with_dc {
continue;
}
quantized[i] = quantize(coeffs[i], factor, i == 0);
dequantized[i] = quantized[i] * factor;
}
(quantized, dequantized)
}
/// Sum of squared differences between a predicted block and the source.
fn sse(pred: &[u8], src: &[i32]) -> u64 {
pred.iter()
.zip(src)
.map(|(&p, &s)| {
let d = i64::from(p) - i64::from(s);
(d * d) as u64
})
.sum()
}
// Sub-block indices address several arrays by geometry; ranges read better than iterators here.
#[allow(clippy::needless_range_loop)]
fn encode_macroblock(&mut self, mbx: usize, mby: usize) {
let (x0, y0) = (mbx * 16, mby * 16);
// Source luma of this macroblock.
let mut src = [0i32; 256];
for r in 0..16 {
for c in 0..16 {
src[r * 16 + c] = self.src_y(x0 + c, y0 + r);
}
}
// Whole-block luma modes: the least squared prediction error wins.
let mut pred16 = [0u8; 256];
let mut best_mode = DC_PRED;
let mut best_sse = u64::MAX;
let mut best_pred = [0u8; 256];
for mode in [DC_PRED, V_PRED, H_PRED, TM_PRED] {
predict_block(&self.rec_y, x0, y0, 16, mode, &mut pred16);
let e = Self::sse(&pred16, &src);
if e < best_sse {
best_sse = e;
best_mode = mode;
best_pred = pred16;
}
}
// Rate-aware comparison with B_PRED: each sub-block mode costs about three bits, and
// the whole-block path codes the DCs once in Y2. lambda scales distortion per bit with
// the quantizer (squared error units).
let lambda = u64::from((self.q.y_ac * self.q.y_ac) as u32) / 8;
let whole_cost = best_sse + 3 * lambda;
// B_PRED: sub-blocks in raster order, each predicted from the reconstruction so far.
let saved_y = self.rec_y.data.clone();
let mut b_modes = [B_DC_PRED; 16];
let mut b_blocks: Vec<Block> = Vec::with_capacity(16);
let mut b_sse = 0u64;
{
for b in 0..16 {
let (bx, by) = (x0 + (b & 3) * 4, y0 + (b >> 2) * 4);
let edges = SubEdges::gather(&self.rec_y, x0, y0, b, self.mb_cols);
let mut sub_src = [0i32; 16];
for r in 0..4 {
for c in 0..4 {
sub_src[r * 4 + c] = src[((b >> 2) * 4 + r) * 16 + (b & 3) * 4 + c];
}
}
let mut best = (u64::MAX, B_DC_PRED, [0u8; 16]);
let mut pred = [0u8; 16];
for mode in 0..10u8 {
predict_sub(&edges, mode, &mut pred);
let e = Self::sse(&pred, &sub_src);
if e < best.0 {
best = (e, mode, pred);
}
}
b_modes[b] = best.1;
b_sse += best.0;
let residual: [i32; 16] = std::array::from_fn(|i| sub_src[i] - i32::from(best.2[i]));
let (quantized, dequantized) = Self::code_block(&residual, self.q.y_dc, self.q.y_ac, true);
let recon = idct(&dequantized);
for r in 0..4 {
for c in 0..4 {
self.rec_y.set(bx + c, by + r, (i32::from(best.2[r * 4 + c]) + recon[r * 4 + c]).clamp(0, 255) as u8);
}
}
b_blocks.push(Block::from_raster(&quantized, 0));
}
}
let b_cost = b_sse + 50 * lambda;
let (y_mode, y2, y_blocks) = if b_cost < whole_cost {
(B_PRED, None, b_blocks)
} else {
// Whole-block mode: undo the B_PRED reconstruction, code 16 blocks + Y2.
self.rec_y.data = saved_y;
let mut dcs = [0i32; 16];
let mut ac_blocks: Vec<[i32; 16]> = Vec::with_capacity(16);
let mut deq_blocks: Vec<[i32; 16]> = Vec::with_capacity(16);
for b in 0..16 {
let mut residual = [0i32; 16];
for r in 0..4 {
for c in 0..4 {
let i = ((b >> 2) * 4 + r) * 16 + (b & 3) * 4 + c;
residual[r * 4 + c] = src[i] - i32::from(best_pred[i]);
}
}
let coeffs = fdct(&residual);
dcs[b] = coeffs[0];
let mut quantized = [0i32; 16];
let mut dequantized = [0i32; 16];
for i in 1..16 {
quantized[i] = quantize(coeffs[i], self.q.y_ac, false);
dequantized[i] = quantized[i] * self.q.y_ac;
}
ac_blocks.push(quantized);
deq_blocks.push(dequantized);
}
// The DCs through the WHT, quantized as Y2, and back through the inverse WHT so the
// reconstruction uses exactly the decoder's DC values.
let y2_coeffs = fwht(&dcs);
let mut y2_q = [0i32; 16];
let mut y2_deq = [0i32; 16];
for i in 0..16 {
let factor = if i == 0 { self.q.y2_dc } else { self.q.y2_ac };
y2_q[i] = quantize(y2_coeffs[i], factor, i == 0);
y2_deq[i] = y2_q[i] * factor;
}
let dc_rec = iwht(&y2_deq);
for b in 0..16 {
deq_blocks[b][0] = dc_rec[b];
let recon = idct(&deq_blocks[b]);
let (bx, by) = (x0 + (b & 3) * 4, y0 + (b >> 2) * 4);
for r in 0..4 {
for c in 0..4 {
let i = ((b >> 2) * 4 + r) * 16 + (b & 3) * 4 + c;
self.rec_y.set(bx + c, by + r, (i32::from(best_pred[i]) + recon[r * 4 + c]).clamp(0, 255) as u8);
}
}
}
let blocks = ac_blocks.iter().map(|q| Block::from_raster(q, 1)).collect();
(best_mode, Some(Block::from_raster(&y2_q, 0)), blocks)
};
// Chroma: one mode for both planes.
let (cx0, cy0) = (mbx * 8, mby * 8);
let mut src_u = [0i32; 64];
let mut src_v = [0i32; 64];
for r in 0..8 {
for c in 0..8 {
src_u[r * 8 + c] = self.src_c(&self.src.u, cx0 + c, cy0 + r);
src_v[r * 8 + c] = self.src_c(&self.src.v, cx0 + c, cy0 + r);
}
}
let mut uv_mode = DC_PRED;
let mut uv_best = u64::MAX;
let mut pred_u = [0u8; 64];
let mut pred_v = [0u8; 64];
let mut pu = [0u8; 64];
let mut pv = [0u8; 64];
for mode in [DC_PRED, V_PRED, H_PRED, TM_PRED] {
predict_block(&self.rec_u, cx0, cy0, 8, mode, &mut pu);
predict_block(&self.rec_v, cx0, cy0, 8, mode, &mut pv);
let e = Self::sse(&pu, &src_u) + Self::sse(&pv, &src_v);
if e < uv_best {
uv_best = e;
uv_mode = mode;
pred_u = pu;
pred_v = pv;
}
}
let mut u_blocks = Vec::with_capacity(4);
let mut v_blocks = Vec::with_capacity(4);
for (plane_src, pred, blocks, is_u) in [(&src_u, &pred_u, &mut u_blocks, true), (&src_v, &pred_v, &mut v_blocks, false)] {
for b in 0..4 {
let mut residual = [0i32; 16];
for r in 0..4 {
for c in 0..4 {
let i = ((b >> 1) * 4 + r) * 8 + (b & 1) * 4 + c;
residual[r * 4 + c] = plane_src[i] - i32::from(pred[i]);
}
}
let (quantized, dequantized) = Self::code_block(&residual, self.q.uv_dc, self.q.uv_ac, true);
let recon = idct(&dequantized);
let (bx, by) = (cx0 + (b & 1) * 4, cy0 + (b >> 1) * 4);
for r in 0..4 {
for c in 0..4 {
let i = ((b >> 1) * 4 + r) * 8 + (b & 1) * 4 + c;
let value = (i32::from(pred[i]) + recon[r * 4 + c]).clamp(0, 255) as u8;
if is_u {
self.rec_u.set(bx + c, by + r, value);
} else {
self.rec_v.set(bx + c, by + r, value);
}
}
}
blocks.push(Block::from_raster(&quantized, 0));
}
}
let skip = y2.as_ref().is_none_or(|b| !b.has_coeffs())
&& y_blocks.iter().all(|b| !b.has_coeffs())
&& u_blocks.iter().all(|b| !b.has_coeffs())
&& v_blocks.iter().all(|b| !b.has_coeffs());
self.mbs.push(Macroblock { y_mode, b_modes, uv_mode, y2, y: y_blocks, u: u_blocks, v: v_blocks, skip });
}
/// Runs `f` over every token bool of the frame in coding order, with the non-zero contexts
/// maintained exactly as the decoder does (RFC 6386 §13.3; skipped macroblocks reset them,
/// leaving Y2 alone for `B_PRED` macroblocks).
fn walk_tokens(&self, mut f: impl FnMut(&Block, usize, usize), mut on_mb: impl FnMut(usize, &Macroblock)) {
let mut above: Vec<Ctx> = vec![[0; 9]; self.mb_cols];
for mby in 0..self.mb_rows {
let mut left: Ctx = [0; 9];
for (mbx, a) in above.iter_mut().enumerate() {
let mb = &self.mbs[mby * self.mb_cols + mbx];
on_mb(mby * self.mb_cols + mbx, mb);
if mb.skip {
for i in 0..8 {
left[i] = 0;
a[i] = 0;
}
if mb.y_mode != B_PRED {
left[8] = 0;
a[8] = 0;
}
continue;
}
if let Some(y2) = &mb.y2 {
let ctx = usize::from(left[8] + a[8]);
f(y2, TYPE_Y2, ctx);
let t = u8::from(y2.has_coeffs());
left[8] = t;
a[8] = t;
}
let ty = if mb.y2.is_some() { TYPE_Y_AFTER_Y2 } else { TYPE_Y_WITH_DC };
for (i, b) in mb.y.iter().enumerate() {
let (ai, li) = (i & 3, i >> 2);
let ctx = usize::from(left[li] + a[ai]);
f(b, ty, ctx);
let t = u8::from(b.has_coeffs());
left[li] = t;
a[ai] = t;
}
for (base, blocks) in [(4usize, &mb.u), (6usize, &mb.v)] {
for (i, b) in blocks.iter().enumerate() {
let (ai, li) = (base + (i & 1), base + (i >> 1));
let ctx = usize::from(left[li] + a[ai]);
f(b, TYPE_UV, ctx);
let t = u8::from(b.has_coeffs());
left[li] = t;
a[ai] = t;
}
}
}
}
}
/// Token probabilities for this frame: the defaults, replaced where the frame's own branch
/// counts give a clearly better estimate (sent as updates in the header).
fn choose_probs(&self, stats: &Stats) -> [[[[u8; 11]; 3]; 8]; 4] {
let mut probs = DEFAULT_COEFF_PROBS;
for (t, plane) in probs.iter_mut().enumerate() {
for (b, band) in plane.iter_mut().enumerate() {
for (c, ctx) in band.iter_mut().enumerate() {
for (n, p) in ctx.iter_mut().enumerate() {
let [zeros, ones] = stats.node[t][b][c][n];
let total = zeros + ones;
if total < 24 {
continue;
}
// Probability of a 0 branch, kept inside 1..=255.
let est = ((u64::from(zeros) * 256 + u64::from(total) / 2) / u64::from(total)).clamp(1, 255) as u8;
// Only worth the update bits when the estimate moves the coding cost.
let cost = |pr: u8| -> f64 {
let p0 = f64::from(pr) / 256.0;
-(f64::from(zeros) * p0.log2() + f64::from(ones) * (1.0 - p0).log2())
};
let update_bits = 8.0 + 1.0;
if cost(*p) - cost(est) > update_bits * 2.0 {
*p = est;
}
}
}
}
}
probs
}
fn write_frame(&self, probs: &[[[[u8; 11]; 3]; 8]; 4], loop_filter_level: u8, qi: u8) -> Vec<u8> {
// First partition: frame header, then per-macroblock prediction records.
let mut h = BoolEncoder::new();
h.put_flag(false); // color_space: BT.601-like
h.put_flag(false); // clamping_type: the decoder clamps
h.put_flag(false); // segmentation_enabled
h.put_flag(false); // filter_type: normal
h.put_literal(6, u32::from(loop_filter_level.min(63)));
h.put_literal(3, 0); // sharpness_level
h.put_flag(false); // loop_filter_adj_enable
h.put_literal(2, 0); // one token partition
h.put_literal(7, u32::from(qi.min(127))); // y_ac_qi
for _ in 0..5 {
h.put_flag(false); // no quantizer deltas
}
h.put_flag(true); // refresh_entropy_probs (meaningless for a single frame)
for t in 0..4 {
for b in 0..8 {
for c in 0..3 {
for n in 0..11 {
let changed = probs[t][b][c][n] != DEFAULT_COEFF_PROBS[t][b][c][n];
h.put(COEFF_UPDATE_PROBS[t][b][c][n], changed);
if changed {
h.put_literal(8, u32::from(probs[t][b][c][n]));
}
}
}
}
}
h.put_flag(true); // mb_no_skip_coeff
let skipped = self.mbs.iter().filter(|m| m.skip).count() as u64;
let total = self.mbs.len().max(1) as u64;
let prob_skip_false = (((total - skipped) * 256 + total / 2) / total).clamp(1, 255) as u8;
h.put_literal(8, u32::from(prob_skip_false));
for mby in 0..self.mb_rows {
for mbx in 0..self.mb_cols {
let mb = &self.mbs[mby * self.mb_cols + mbx];
h.put(prob_skip_false, mb.skip);
h.put_tree(KF_YMODE_TREE, &KF_YMODE_PROB, mb.y_mode);
if mb.y_mode == B_PRED {
for b in 0..16 {
let above = if b >= 4 {
mb.b_modes[b - 4]
} else if mby == 0 {
B_DC_PRED
} else {
self.sub_mode_of(&self.mbs[(mby - 1) * self.mb_cols + mbx], b + 12)
};
let left = if b & 3 != 0 {
mb.b_modes[b - 1]
} else if mbx == 0 {
B_DC_PRED
} else {
self.sub_mode_of(&self.mbs[mby * self.mb_cols + mbx - 1], b + 3)
};
h.put_tree(BMODE_TREE, &KF_BMODE_PROBS[usize::from(above)][usize::from(left)], mb.b_modes[b]);
}
}
h.put_tree(UV_MODE_TREE, &KF_UV_MODE_PROB, mb.uv_mode);
}
}
let first = h.finish();
// Token partition.
let mut t = BoolEncoder::new();
self.walk_tokens(
|block, ty, ctx0| {
for_each_token_bool(block, ty, ctx0, |ev| match ev {
Event::Node(ty, band, ctx, node, bit) => t.put(probs[ty][band][ctx][node], bit),
Event::Extra(cat, bits) => {
let p = PCAT[cat];
for (i, &prob) in p.iter().enumerate() {
let shift = p.len() - 1 - i;
t.put(prob, (bits >> shift) & 1 != 0);
}
}
Event::Sign(negative) => t.put_flag(negative),
});
},
|_, _| {},
);
let tokens = t.finish();
// Frame tag, start code, dimensions, partitions.
let mut out = Vec::with_capacity(10 + first.len() + tokens.len());
let size = (first.len() as u32).min((1 << 19) - 1);
// Bit 0 = 0: key frame; bits 1-3 = 0: version 0; bit 4: show_frame; then the size.
let tag: u32 = (1 << 4) | (size << 5);
out.extend_from_slice(&tag.to_le_bytes()[..3]);
out.extend_from_slice(&[0x9d, 0x01, 0x2a]);
out.extend_from_slice(&(self.src.width as u16).to_le_bytes());
out.extend_from_slice(&(self.src.height as u16).to_le_bytes());
out.extend_from_slice(&first);
out.extend_from_slice(&tokens);
out
}
/// The sub-block mode context a neighbouring macroblock provides for sub-block `b` (an
/// index into the neighbour's 16 sub-blocks): its own sub-block mode when it is `B_PRED`,
/// else the sub-block equivalent of its whole-block mode (RFC 6386 §11.3).
fn sub_mode_of(&self, mb: &Macroblock, b: usize) -> u8 {
match mb.y_mode {
B_PRED => mb.b_modes[b.min(15)],
V_PRED => B_VE_PRED,
H_PRED => B_HE_PRED,
TM_PRED => B_TM_PRED,
_ => B_DC_PRED,
}
}
}
/// Encodes `yuv` as one VP8 key frame (the payload of a WebP `VP8 ` chunk).
pub fn encode_frame(yuv: &Yuv, params: Params) -> Result<Vec<u8>, Vp8Error> {
if yuv.width == 0 || yuv.height == 0 || yuv.width > MAX_DIMENSION || yuv.height > MAX_DIMENSION {
return Err(Vp8Error::Dimensions);
}
let (w, h) = (yuv.width as usize, yuv.height as usize);
if yuv.y.len() != w * h || yuv.u.len() != w.div_ceil(2) * h.div_ceil(2) || yuv.v.len() != yuv.u.len() {
return Err(Vp8Error::Planes);
}
let qi = quantizer_index(params.quality);
let mut enc = Encoder::new(yuv, qi);
for mby in 0..enc.mb_rows {
for mbx in 0..enc.mb_cols {
enc.encode_macroblock(mbx, mby);
}
}
// Pass 1 over the tokens: branch statistics → this frame's probabilities.
let mut stats = Stats::new();
enc.walk_tokens(
|block, ty, ctx0| {
for_each_token_bool(block, ty, ctx0, |ev| {
if let Event::Node(ty, band, ctx, node, bit) = ev {
stats.node[ty][band][ctx][node][usize::from(bit)] += 1;
}
});
},
|_, _| {},
);
let probs = enc.choose_probs(&stats);
// The loop filter smooths block edges in proportion to the quantizer; none at the finest.
let loop_filter_level = if qi == 0 { 0 } else { (u32::from(qi) / 2 + 4).min(63) as u8 };
Ok(enc.write_frame(&probs, loop_filter_level, qi))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quantizer_curve_is_monotone_and_spans_the_range() {
assert_eq!(quantizer_index(100), 0);
assert_eq!(quantizer_index(0), 127);
let mut last = 0;
for q in (0..=100).rev() {
let qi = quantizer_index(q);
assert!(qi >= last, "quality {q}");
last = qi;
}
assert!((8..=14).contains(&quantizer_index(90)), "{}", quantizer_index(90));
assert!((24..=32).contains(&quantizer_index(75)), "{}", quantizer_index(75));
}
#[test]
fn tokens_cover_every_magnitude() {
for v in [0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 18, 19, 34, 35, 66, 67, 1000, 2048] {
let (tok, extra) = token_of(v);
let base = match extra {
None => i32::from(tok),
Some((c, bits)) => {
assert!(bits < (1 << PCAT[c].len()), "{v}");
CAT_BASE[c] + bits as i32
}
};
assert_eq!(base, v);
assert_eq!(token_of(-v).0, tok);
}
}
#[test]
fn block_eob_is_after_the_last_nonzero() {
let mut raster = [0i32; 16];
let b = Block::from_raster(&raster, 1);
assert_eq!(b.eob, 1);
assert!(!b.has_coeffs());
raster[ZIGZAG[5]] = 3;
let b = Block::from_raster(&raster, 0);
assert_eq!(b.eob, 6);
assert!(b.has_coeffs());
raster[ZIGZAG[15]] = -1;
assert_eq!(Block::from_raster(&raster, 0).eob, 16);
}
#[test]
fn rejects_bad_dimensions() {
let y = Yuv { width: 0, height: 4, y: vec![], u: vec![], v: vec![] };
assert_eq!(encode_frame(&y, Params { quality: 80 }).unwrap_err(), Vp8Error::Dimensions);
let y = Yuv { width: 4, height: 4, y: vec![0; 15], u: vec![0; 4], v: vec![0; 4] };
assert_eq!(encode_frame(&y, Params { quality: 80 }).unwrap_err(), Vp8Error::Planes);
}
#[test]
fn frame_header_is_well_formed() {
let rgb: Vec<u8> = (0..(20 * 13 * 3)).map(|i| (i * 7 % 256) as u8).collect();
let yuv = Yuv::from_rgb(&rgb, 20, 13, 3);
let frame = encode_frame(&yuv, Params { quality: 75 }).unwrap();
assert_eq!(frame[0] & 1, 0, "key frame");
assert_eq!((frame[0] >> 4) & 1, 1, "shown");
assert_eq!(&frame[3..6], &[0x9d, 0x01, 0x2a]);
assert_eq!(u16::from_le_bytes([frame[6], frame[7]]) & 0x3fff, 20);
assert_eq!(u16::from_le_bytes([frame[8], frame[9]]) & 0x3fff, 13);
let first_size = (u32::from(frame[0]) | (u32::from(frame[1]) << 8) | (u32::from(frame[2]) << 16)) >> 5;
assert!((10 + first_size as usize) < frame.len());
}
}

View File

@ -0,0 +1,380 @@
//! Intra prediction (RFC 6386 §12): whole-macroblock luma and chroma modes, and the ten 4×4
//! sub-block modes. Predictions read the frame's *unfiltered* reconstruction, which is what the
//! decoder predicts from too (the loop filter runs on the finished frame).
use super::tables::*;
/// A plane of the reconstruction with the virtual border VP8 defines: the row above the frame
/// reads as 127, the column left of it as 129, and the pixel above-left of the frame as 127.
pub struct Plane {
pub data: Vec<u8>,
pub width: usize,
}
impl Plane {
pub fn new(width: usize, height: usize) -> Self {
Plane { data: vec![0; width * height], width }
}
#[inline]
pub fn at(&self, x: isize, y: isize) -> u8 {
if y < 0 {
127
} else if x < 0 {
129
} else {
self.data.get(y as usize * self.width + x as usize).copied().unwrap_or(127)
}
}
/// The row above an `n`-wide block at (`x`, `y`), plus the four pixels to its upper right
/// (sub-block modes use them; `avail_right` says whether those exist in the frame yet).
pub fn above(&self, x: usize, y: usize, n: usize, out: &mut [u8]) {
for (i, o) in out.iter_mut().enumerate().take(n) {
*o = self.at((x + i) as isize, y as isize - 1);
}
}
pub fn left(&self, x: usize, y: usize, n: usize, out: &mut [u8]) {
for (i, o) in out.iter_mut().enumerate().take(n) {
*o = self.at(x as isize - 1, (y + i) as isize);
}
}
/// The pixel above-left of (`x`, `y`).
pub fn corner(&self, x: usize, y: usize) -> u8 {
self.at(x as isize - 1, y as isize - 1)
}
#[inline]
pub fn set(&mut self, x: usize, y: usize, v: u8) {
if let Some(p) = self.data.get_mut(y * self.width + x) {
*p = v;
}
}
}
#[inline]
fn clamp255(v: i32) -> u8 {
v.clamp(0, 255) as u8
}
/// Whole-block prediction of an `n × n` block at (`x`, `y`) in `plane` (16 for luma, 8 for chroma)
/// with one of DC / V / H / TM, written row-major into `out`.
pub fn predict_block(plane: &Plane, x: usize, y: usize, n: usize, mode: u8, out: &mut [u8]) {
let mut above = [0u8; 16];
let mut left = [0u8; 16];
plane.above(x, y, n, &mut above);
plane.left(x, y, n, &mut left);
let top_row = y == 0;
let left_col = x == 0;
match mode {
V_PRED => {
for r in 0..n {
out[r * n..r * n + n].copy_from_slice(&above[..n]);
}
}
H_PRED => {
for r in 0..n {
out[r * n..r * n + n].fill(left[r]);
}
}
TM_PRED => {
let p = i32::from(plane.corner(x, y));
for r in 0..n {
for c in 0..n {
out[r * n + c] = clamp255(i32::from(left[r]) + i32::from(above[c]) - p);
}
}
}
_ => {
// DC: the average of the visible edge pixels; 128 in the top-left corner.
let shift = if n == 16 { 4 } else { 3 };
let v = match (top_row, left_col) {
(true, true) => 128,
(true, false) => {
let s: u32 = left[..n].iter().map(|&v| u32::from(v)).sum();
((s + (1 << (shift - 1))) >> shift) as u8
}
(false, true) => {
let s: u32 = above[..n].iter().map(|&v| u32::from(v)).sum();
((s + (1 << (shift - 1))) >> shift) as u8
}
(false, false) => {
let s: u32 = above[..n].iter().chain(&left[..n]).map(|&v| u32::from(v)).sum();
((s + (1 << shift)) >> (shift + 1)) as u8
}
};
out[..n * n].fill(v);
}
}
}
#[inline]
fn avg2(a: u8, b: u8) -> u8 {
((u16::from(a) + u16::from(b) + 1) >> 1) as u8
}
#[inline]
fn avg3(a: u8, b: u8, c: u8) -> u8 {
((u16::from(a) + 2 * u16::from(b) + u16::from(c) + 2) >> 2) as u8
}
/// The edge pixels a 4×4 sub-block prediction reads: `above[0..8]` (the four above and the four
/// above-right), `left[0..4]`, and the corner `p` above-left.
pub struct SubEdges {
pub above: [u8; 8],
pub left: [u8; 4],
pub p: u8,
}
impl SubEdges {
/// Edges of sub-block `b` (0..16, raster) of the macroblock at (`mx`, `my`) in luma pixels.
/// The above-right pixels of the right-column sub-blocks come from the row above the
/// macroblock (positions (-1, 16..20)), replicated from (-1, 15) for the rightmost macroblock
/// (RFC 6386 §12.3); for the top macroblock row they read 127 like every above pixel.
pub fn gather(plane: &Plane, mx: usize, my: usize, b: usize, mb_cols: usize) -> SubEdges {
let bx = mx + (b & 3) * 4;
let by = my + (b >> 2) * 4;
let mut above = [0u8; 8];
let mut left = [0u8; 4];
plane.above(bx, by, 4, &mut above);
plane.left(bx, by, 4, &mut left);
let p = plane.corner(bx, by);
if b & 3 == 3 {
// Right column: the above-right pixels are those above the macroblock's right edge.
let last_col = mx / 16 + 1 == mb_cols;
for i in 0..4 {
let ax = if last_col { mx as isize + 15 } else { mx as isize + 16 + i as isize };
above[4 + i] = plane.at(ax, my as isize - 1);
}
} else {
for i in 0..4 {
above[4 + i] = plane.at((bx + 4 + i) as isize, by as isize - 1);
}
}
SubEdges { above, left, p }
}
}
/// Predicts one 4×4 sub-block with `mode` into `out` (row-major), RFC 6386 §12.3.
// Written as the specification's position-by-position assignments, which index on purpose.
#[allow(clippy::needless_range_loop)]
pub fn predict_sub(e: &SubEdges, mode: u8, out: &mut [u8; 16]) {
let a = &e.above;
let l = &e.left;
// E: the nine edge pixels from bottom-left, through the corner, to the right.
let ed = [l[3], l[2], l[1], l[0], e.p, a[0], a[1], a[2], a[3]];
// avg3 centred on E[i], avg2 of E[i] and E[i + 1].
let e3 = |i: usize| avg3(ed[i - 1], ed[i], ed[i + 1]);
let e2 = |i: usize| avg2(ed[i], ed[i + 1]);
// The same over the above row (A[-1] is P).
let ap = |i: isize| if i < 0 { e.p } else { a[i as usize] };
let a3 = |i: isize| avg3(ap(i - 1), ap(i), ap(i + 1));
let a2 = |i: isize| avg2(ap(i), ap(i + 1));
let lp = |i: isize| if i < 0 { e.p } else { l[i as usize] };
let l3 = |i: isize| avg3(lp(i - 1), lp(i), lp(i + 1));
let l2 = |i: isize| avg2(lp(i), lp(i + 1));
let mut b = [[0u8; 4]; 4];
match mode {
B_TM_PRED => {
for r in 0..4 {
for c in 0..4 {
b[r][c] = clamp255(i32::from(l[r]) + i32::from(a[c]) - i32::from(e.p));
}
}
}
B_VE_PRED => {
for c in 0..4 {
let v = a3(c as isize);
for r in 0..4 {
b[r][c] = v;
}
}
}
B_HE_PRED => {
let rows = [l3(0), l3(1), l3(2), avg3(l[2], l[3], l[3])];
for r in 0..4 {
b[r] = [rows[r]; 4];
}
}
B_LD_PRED => {
b[0][0] = a3(1);
b[0][1] = a3(2);
b[1][0] = b[0][1];
b[0][2] = a3(3);
b[1][1] = b[0][2];
b[2][0] = b[0][2];
b[0][3] = a3(4);
b[1][2] = b[0][3];
b[2][1] = b[0][3];
b[3][0] = b[0][3];
b[1][3] = a3(5);
b[2][2] = b[1][3];
b[3][1] = b[1][3];
b[2][3] = a3(6);
b[3][2] = b[2][3];
b[3][3] = avg3(a[6], a[7], a[7]);
}
B_RD_PRED => {
b[3][0] = e3(1);
b[3][1] = e3(2);
b[2][0] = b[3][1];
b[3][2] = e3(3);
b[2][1] = b[3][2];
b[1][0] = b[3][2];
b[3][3] = e3(4);
b[2][2] = b[3][3];
b[1][1] = b[3][3];
b[0][0] = b[3][3];
b[2][3] = e3(5);
b[1][2] = b[2][3];
b[0][1] = b[2][3];
b[1][3] = e3(6);
b[0][2] = b[1][3];
b[0][3] = e3(7);
}
B_VR_PRED => {
b[3][0] = e3(2);
b[2][0] = e3(3);
b[3][1] = e3(4);
b[1][0] = b[3][1];
b[2][1] = e2(4);
b[0][0] = b[2][1];
b[3][2] = e3(5);
b[1][1] = b[3][2];
b[2][2] = e2(5);
b[0][1] = b[2][2];
b[3][3] = e3(6);
b[1][2] = b[3][3];
b[2][3] = e2(6);
b[0][2] = b[2][3];
b[1][3] = e3(7);
b[0][3] = e2(7);
}
B_VL_PRED => {
b[0][0] = a2(0);
b[1][0] = a3(1);
b[2][0] = a2(1);
b[0][1] = b[2][0];
b[1][1] = a3(2);
b[3][0] = b[1][1];
b[2][1] = a2(2);
b[0][2] = b[2][1];
b[3][1] = a3(3);
b[1][2] = b[3][1];
b[2][2] = a2(3);
b[0][3] = b[2][2];
b[3][2] = a3(4);
b[1][3] = b[3][2];
b[2][3] = a3(5);
b[3][3] = a3(6);
}
B_HD_PRED => {
b[3][0] = e2(0);
b[3][1] = e3(1);
b[2][0] = e2(1);
b[3][2] = b[2][0];
b[2][1] = e3(2);
b[3][3] = b[2][1];
b[2][2] = e2(2);
b[1][0] = b[2][2];
b[2][3] = e3(3);
b[1][1] = b[2][3];
b[1][2] = e2(3);
b[0][0] = b[1][2];
b[1][3] = e3(4);
b[0][1] = b[1][3];
b[0][2] = e3(5);
b[0][3] = e3(6);
}
B_HU_PRED => {
b[0][0] = l2(0);
b[0][1] = l3(1);
b[0][2] = l2(1);
b[1][0] = b[0][2];
b[0][3] = l3(2);
b[1][1] = b[0][3];
b[1][2] = l2(2);
b[2][0] = b[1][2];
b[1][3] = avg3(l[2], l[3], l[3]);
b[2][1] = b[1][3];
b[2][2] = l[3];
b[2][3] = l[3];
b[3] = [l[3]; 4];
}
_ => {
// B_DC_PRED: the average of the four above and four left pixels.
let s: u32 = a[..4].iter().chain(&l[..]).map(|&v| u32::from(v)).sum();
b = [[((s + 4) >> 3) as u8; 4]; 4];
}
}
for r in 0..4 {
out[r * 4..r * 4 + 4].copy_from_slice(&b[r]);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn border_values() {
let p = Plane::new(16, 16);
assert_eq!(p.at(-1, -1), 127);
assert_eq!(p.at(5, -1), 127);
assert_eq!(p.at(-1, 5), 129);
assert_eq!(p.corner(0, 0), 127);
assert_eq!(p.corner(0, 16), 129);
assert_eq!(p.corner(16, 0), 127);
}
#[test]
fn dc_prediction_edge_cases() {
let mut p = Plane::new(32, 32);
for y in 0..32 {
for x in 0..32 {
p.set(x, y, if y < 16 { 10 } else { 200 });
}
}
let mut out = [0u8; 256];
predict_block(&p, 0, 0, 16, DC_PRED, &mut out);
assert!(out.iter().all(|&v| v == 128), "top-left is 128");
predict_block(&p, 16, 0, 16, DC_PRED, &mut out);
assert!(out.iter().all(|&v| v == 10), "top row averages the left column only");
predict_block(&p, 0, 16, 16, DC_PRED, &mut out);
assert!(out.iter().all(|&v| v == 10), "left column averages the above row only");
predict_block(&p, 16, 16, 16, DC_PRED, &mut out);
assert!(out.iter().all(|&v| v == (10 + 200) / 2));
predict_block(&p, 16, 16, 16, TM_PRED, &mut out);
// L + A - P = 200 + 10 - 10 = 200 everywhere.
assert!(out.iter().all(|&v| v == 200));
}
#[test]
fn sub_block_modes_are_in_range_and_dc_averages() {
let mut p = Plane::new(32, 32);
for y in 0..32 {
for x in 0..32 {
p.set(x, y, ((x * 7 + y * 13) % 256) as u8);
}
}
for b in 0..16 {
let e = SubEdges::gather(&p, 16, 16, b, 2);
let mut out = [0u8; 16];
for mode in 0..10u8 {
predict_sub(&e, mode, &mut out);
}
predict_sub(&e, B_DC_PRED, &mut out);
let s: u32 = e.above[..4].iter().chain(&e.left).map(|&v| u32::from(v)).sum();
assert!(out.iter().all(|&v| u32::from(v) == (s + 4) >> 3));
}
// Rightmost macroblock: the above-right pixels replicate (-1, 15).
let e = SubEdges::gather(&p, 16, 16, 3, 2);
assert!(e.above[4..].iter().all(|&v| v == p.at(31, 15)));
// Top row: everything above is 127.
let e = SubEdges::gather(&p, 0, 0, 3, 2);
assert!(e.above.iter().all(|&v| v == 127));
assert_eq!(e.p, 127);
}
}

View File

@ -0,0 +1,593 @@
//! Constant tables of the VP8 bitstream, as published in RFC 6386. The four large tables were
//! generated from the RFC text (sections 11.5, 13.4, 13.5 and 14.1); the small ones are
//! transcribed from sections 11.2, 11.4, 13.2 and 13.3.
/// Luma intra modes of a macroblock (RFC 6386 §11.2), in the order of `intra_mbmode`.
pub const DC_PRED: u8 = 0;
pub const V_PRED: u8 = 1;
pub const H_PRED: u8 = 2;
pub const TM_PRED: u8 = 3;
pub const B_PRED: u8 = 4;
/// Sub-block intra modes (RFC 6386 §11.2), in the order of `intra_bmode`.
pub const B_DC_PRED: u8 = 0;
pub const B_TM_PRED: u8 = 1;
pub const B_VE_PRED: u8 = 2;
pub const B_HE_PRED: u8 = 3;
pub const B_LD_PRED: u8 = 4;
pub const B_RD_PRED: u8 = 5;
pub const B_VR_PRED: u8 = 6;
pub const B_VL_PRED: u8 = 7;
pub const B_HD_PRED: u8 = 8;
pub const B_HU_PRED: u8 = 9;
/// A coding tree as in the RFC: positive entries index the next node pair, `-leaf` entries end
/// a path (the leaf value is negated; `-0` is written as the sentinel below).
pub type Tree = &'static [i8];
/// `kf_ymode_tree`: `B_PRED` = "0", then DC "100", V "101", H "110", TM "111".
pub const KF_YMODE_TREE: Tree = &[-(B_PRED as i8), 2, 4, 6, -(DC_PRED as i8), -(V_PRED as i8), -(H_PRED as i8), -(TM_PRED as i8)];
pub const KF_YMODE_PROB: [u8; 4] = [145, 156, 163, 128];
/// `uv_mode_tree`: DC "0", V "10", H "110", TM "111".
pub const UV_MODE_TREE: Tree = &[-(DC_PRED as i8), 2, -(V_PRED as i8), 4, -(H_PRED as i8), -(TM_PRED as i8)];
pub const KF_UV_MODE_PROB: [u8; 3] = [142, 114, 183];
/// `bmode_tree` (RFC 6386 §11.2).
pub const BMODE_TREE: Tree = &[
-(B_DC_PRED as i8),
2,
-(B_TM_PRED as i8),
4,
-(B_VE_PRED as i8),
6,
8,
12,
-(B_HE_PRED as i8),
10,
-(B_RD_PRED as i8),
-(B_VR_PRED as i8),
-(B_LD_PRED as i8),
14,
-(B_VL_PRED as i8),
16,
-(B_HD_PRED as i8),
-(B_HU_PRED as i8),
];
/// Coefficient tokens (RFC 6386 §13.2).
pub const DCT_0: u8 = 0;
pub const DCT_1: u8 = 1;
pub const DCT_2: u8 = 2;
pub const DCT_3: u8 = 3;
pub const DCT_4: u8 = 4;
pub const DCT_CAT1: u8 = 5;
pub const DCT_CAT2: u8 = 6;
pub const DCT_CAT3: u8 = 7;
pub const DCT_CAT4: u8 = 8;
pub const DCT_CAT5: u8 = 9;
pub const DCT_CAT6: u8 = 10;
pub const DCT_EOB: u8 = 11;
/// `coeff_tree` (RFC 6386 §13.2). The EOB leaf is at index 0, so the tree is also walked from
/// index 2 when an EOB cannot follow (after a zero token).
pub const COEFF_TREE: Tree = &[
-(DCT_EOB as i8),
2,
-(DCT_0 as i8),
4,
-(DCT_1 as i8),
6,
8,
12,
-(DCT_2 as i8),
10,
-(DCT_3 as i8),
-(DCT_4 as i8),
14,
16,
-(DCT_CAT1 as i8),
-(DCT_CAT2 as i8),
18,
20,
-(DCT_CAT3 as i8),
-(DCT_CAT4 as i8),
-(DCT_CAT5 as i8),
-(DCT_CAT6 as i8),
];
/// Extra-bit probabilities of the six value categories (RFC 6386 §13.2), most significant bit
/// first; the category bases are 5, 7, 11, 19, 35 and 67.
pub const PCAT: [&[u8]; 6] =
[&[159], &[165, 145], &[173, 148, 140], &[176, 155, 140, 135], &[180, 157, 141, 134, 130], &[254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129]];
pub const CAT_BASE: [i32; 6] = [5, 7, 11, 19, 35, 67];
/// Coefficient position → probability band (RFC 6386 §13.3).
pub const COEFF_BANDS: [usize; 16] = [0, 1, 2, 3, 6, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7];
/// Zig-zag scan: the raster index of the coefficient coded at each position (RFC 6386 §13).
pub const ZIGZAG: [usize; 16] = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15];
/// Default token probabilities `[plane][band][context][token]` (RFC 6386 §13.5).
pub const DEFAULT_COEFF_PROBS: [[[[u8; 11]; 3]; 8]; 4] = [
[
[
[128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128],
[128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128],
[128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128],
],
[
[253, 136, 254, 255, 228, 219, 128, 128, 128, 128, 128],
[189, 129, 242, 255, 227, 213, 255, 219, 128, 128, 128],
[106, 126, 227, 252, 214, 209, 255, 255, 128, 128, 128],
],
[
[1, 98, 248, 255, 236, 226, 255, 255, 128, 128, 128],
[181, 133, 238, 254, 221, 234, 255, 154, 128, 128, 128],
[78, 134, 202, 247, 198, 180, 255, 219, 128, 128, 128],
],
[
[1, 185, 249, 255, 243, 255, 128, 128, 128, 128, 128],
[184, 150, 247, 255, 236, 224, 128, 128, 128, 128, 128],
[77, 110, 216, 255, 236, 230, 128, 128, 128, 128, 128],
],
[
[1, 101, 251, 255, 241, 255, 128, 128, 128, 128, 128],
[170, 139, 241, 252, 236, 209, 255, 255, 128, 128, 128],
[37, 116, 196, 243, 228, 255, 255, 255, 128, 128, 128],
],
[
[1, 204, 254, 255, 245, 255, 128, 128, 128, 128, 128],
[207, 160, 250, 255, 238, 128, 128, 128, 128, 128, 128],
[102, 103, 231, 255, 211, 171, 128, 128, 128, 128, 128],
],
[
[1, 152, 252, 255, 240, 255, 128, 128, 128, 128, 128],
[177, 135, 243, 255, 234, 225, 128, 128, 128, 128, 128],
[80, 129, 211, 255, 194, 224, 128, 128, 128, 128, 128],
],
[
[1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128],
[246, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128],
[255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128],
],
],
[
[
[198, 35, 237, 223, 193, 187, 162, 160, 145, 155, 62],
[131, 45, 198, 221, 172, 176, 220, 157, 252, 221, 1],
[68, 47, 146, 208, 149, 167, 221, 162, 255, 223, 128],
],
[
[1, 149, 241, 255, 221, 224, 255, 255, 128, 128, 128],
[184, 141, 234, 253, 222, 220, 255, 199, 128, 128, 128],
[81, 99, 181, 242, 176, 190, 249, 202, 255, 255, 128],
],
[
[1, 129, 232, 253, 214, 197, 242, 196, 255, 255, 128],
[99, 121, 210, 250, 201, 198, 255, 202, 128, 128, 128],
[23, 91, 163, 242, 170, 187, 247, 210, 255, 255, 128],
],
[
[1, 200, 246, 255, 234, 255, 128, 128, 128, 128, 128],
[109, 178, 241, 255, 231, 245, 255, 255, 128, 128, 128],
[44, 130, 201, 253, 205, 192, 255, 255, 128, 128, 128],
],
[
[1, 132, 239, 251, 219, 209, 255, 165, 128, 128, 128],
[94, 136, 225, 251, 218, 190, 255, 255, 128, 128, 128],
[22, 100, 174, 245, 186, 161, 255, 199, 128, 128, 128],
],
[
[1, 182, 249, 255, 232, 235, 128, 128, 128, 128, 128],
[124, 143, 241, 255, 227, 234, 128, 128, 128, 128, 128],
[35, 77, 181, 251, 193, 211, 255, 205, 128, 128, 128],
],
[
[1, 157, 247, 255, 236, 231, 255, 255, 128, 128, 128],
[121, 141, 235, 255, 225, 227, 255, 255, 128, 128, 128],
[45, 99, 188, 251, 195, 217, 255, 224, 128, 128, 128],
],
[
[1, 1, 251, 255, 213, 255, 128, 128, 128, 128, 128],
[203, 1, 248, 255, 255, 128, 128, 128, 128, 128, 128],
[137, 1, 177, 255, 224, 255, 128, 128, 128, 128, 128],
],
],
[
[
[253, 9, 248, 251, 207, 208, 255, 192, 128, 128, 128],
[175, 13, 224, 243, 193, 185, 249, 198, 255, 255, 128],
[73, 17, 171, 221, 161, 179, 236, 167, 255, 234, 128],
],
[
[1, 95, 247, 253, 212, 183, 255, 255, 128, 128, 128],
[239, 90, 244, 250, 211, 209, 255, 255, 128, 128, 128],
[155, 77, 195, 248, 188, 195, 255, 255, 128, 128, 128],
],
[
[1, 24, 239, 251, 218, 219, 255, 205, 128, 128, 128],
[201, 51, 219, 255, 196, 186, 128, 128, 128, 128, 128],
[69, 46, 190, 239, 201, 218, 255, 228, 128, 128, 128],
],
[
[1, 191, 251, 255, 255, 128, 128, 128, 128, 128, 128],
[223, 165, 249, 255, 213, 255, 128, 128, 128, 128, 128],
[141, 124, 248, 255, 255, 128, 128, 128, 128, 128, 128],
],
[
[1, 16, 248, 255, 255, 128, 128, 128, 128, 128, 128],
[190, 36, 230, 255, 236, 255, 128, 128, 128, 128, 128],
[149, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128],
],
[
[1, 226, 255, 128, 128, 128, 128, 128, 128, 128, 128],
[247, 192, 255, 128, 128, 128, 128, 128, 128, 128, 128],
[240, 128, 255, 128, 128, 128, 128, 128, 128, 128, 128],
],
[
[1, 134, 252, 255, 255, 128, 128, 128, 128, 128, 128],
[213, 62, 250, 255, 255, 128, 128, 128, 128, 128, 128],
[55, 93, 255, 128, 128, 128, 128, 128, 128, 128, 128],
],
[
[128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128],
[128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128],
[128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128],
],
],
[
[
[202, 24, 213, 235, 186, 191, 220, 160, 240, 175, 255],
[126, 38, 182, 232, 169, 184, 228, 174, 255, 187, 128],
[61, 46, 138, 219, 151, 178, 240, 170, 255, 216, 128],
],
[
[1, 112, 230, 250, 199, 191, 247, 159, 255, 255, 128],
[166, 109, 228, 252, 211, 215, 255, 174, 128, 128, 128],
[39, 77, 162, 232, 172, 180, 245, 178, 255, 255, 128],
],
[
[1, 52, 220, 246, 198, 199, 249, 220, 255, 255, 128],
[124, 74, 191, 243, 183, 193, 250, 221, 255, 255, 128],
[24, 71, 130, 219, 154, 170, 243, 182, 255, 255, 128],
],
[
[1, 182, 225, 249, 219, 240, 255, 224, 128, 128, 128],
[149, 150, 226, 252, 216, 205, 255, 171, 128, 128, 128],
[28, 108, 170, 242, 183, 194, 254, 223, 255, 255, 128],
],
[
[1, 81, 230, 252, 204, 203, 255, 192, 128, 128, 128],
[123, 102, 209, 247, 188, 196, 255, 233, 128, 128, 128],
[20, 95, 153, 243, 164, 173, 255, 203, 128, 128, 128],
],
[
[1, 222, 248, 255, 216, 213, 128, 128, 128, 128, 128],
[168, 175, 246, 252, 235, 205, 255, 255, 128, 128, 128],
[47, 116, 215, 255, 211, 212, 255, 255, 128, 128, 128],
],
[
[1, 121, 236, 253, 212, 214, 255, 255, 128, 128, 128],
[141, 84, 213, 252, 201, 202, 255, 219, 128, 128, 128],
[42, 80, 160, 240, 162, 185, 255, 205, 128, 128, 128],
],
[
[1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128],
[244, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128],
[238, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128],
],
],
];
/// Probabilities with which each token probability's update flag is coded (RFC 6386 §13.4).
pub const COEFF_UPDATE_PROBS: [[[[u8; 11]; 3]; 8]; 4] = [
[
[
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[176, 246, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[223, 241, 252, 255, 255, 255, 255, 255, 255, 255, 255],
[249, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 244, 252, 255, 255, 255, 255, 255, 255, 255, 255],
[234, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 246, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[239, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[251, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[251, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 254, 253, 255, 254, 255, 255, 255, 255, 255, 255],
[250, 255, 254, 255, 254, 255, 255, 255, 255, 255, 255],
[254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
],
[
[
[217, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[225, 252, 241, 253, 255, 255, 254, 255, 255, 255, 255],
[234, 250, 241, 250, 253, 255, 253, 254, 255, 255, 255],
],
[
[255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[223, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[238, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[249, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[247, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255],
[250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
],
[
[
[186, 251, 250, 255, 255, 255, 255, 255, 255, 255, 255],
[234, 251, 244, 254, 255, 255, 255, 255, 255, 255, 255],
[251, 251, 243, 253, 254, 255, 254, 255, 255, 255, 255],
],
[
[255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[236, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[251, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255],
],
[
[255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
],
[
[
[248, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[250, 254, 252, 254, 255, 255, 255, 255, 255, 255, 255],
[248, 254, 249, 253, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255],
[246, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255],
[252, 254, 251, 254, 254, 255, 255, 255, 255, 255, 255],
],
[
[255, 254, 252, 255, 255, 255, 255, 255, 255, 255, 255],
[248, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255],
[253, 255, 254, 254, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[245, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[253, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255],
[252, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 252, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[249, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 255, 253, 255, 255, 255, 255, 255, 255, 255, 255],
[250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
[
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
],
],
];
/// DC dequantization factors by quantizer index (RFC 6386 §14.1).
pub const DC_QLOOKUP: [i32; 128] = [
4, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 14, 15, 16, 17, 17, 18, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
37, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73,
74, 75, 76, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 93, 95, 96, 98, 100, 101, 102, 104, 106, 108, 110, 112, 114, 116, 118, 122, 124,
126, 128, 130, 132, 134, 136, 138, 140, 143, 145, 148, 151, 154, 157,
];
/// AC dequantization factors by quantizer index (RFC 6386 §14.1).
pub const AC_QLOOKUP: [i32; 128] = [
4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104,
106, 108, 110, 112, 114, 116, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 155, 158, 161, 164, 167, 170, 173, 177, 181, 185, 189, 193, 197,
201, 205, 209, 213, 217, 221, 225, 229, 234, 239, 245, 249, 254, 259, 264, 269, 274, 279, 284,
];
/// Key-frame sub-block mode probabilities `[above mode][left mode][tree node]` (RFC 6386 §11.5).
pub const KF_BMODE_PROBS: [[[u8; 9]; 10]; 10] = [
[
[231, 120, 48, 89, 115, 113, 120, 152, 112],
[152, 179, 64, 126, 170, 118, 46, 70, 95],
[175, 69, 143, 80, 85, 82, 72, 155, 103],
[56, 58, 10, 171, 218, 189, 17, 13, 152],
[144, 71, 10, 38, 171, 213, 144, 34, 26],
[114, 26, 17, 163, 44, 195, 21, 10, 173],
[121, 24, 80, 195, 26, 62, 44, 64, 85],
[170, 46, 55, 19, 136, 160, 33, 206, 71],
[63, 20, 8, 114, 114, 208, 12, 9, 226],
[81, 40, 11, 96, 182, 84, 29, 16, 36],
],
[
[134, 183, 89, 137, 98, 101, 106, 165, 148],
[72, 187, 100, 130, 157, 111, 32, 75, 80],
[66, 102, 167, 99, 74, 62, 40, 234, 128],
[41, 53, 9, 178, 241, 141, 26, 8, 107],
[104, 79, 12, 27, 217, 255, 87, 17, 7],
[74, 43, 26, 146, 73, 166, 49, 23, 157],
[65, 38, 105, 160, 51, 52, 31, 115, 128],
[87, 68, 71, 44, 114, 51, 15, 186, 23],
[47, 41, 14, 110, 182, 183, 21, 17, 194],
[66, 45, 25, 102, 197, 189, 23, 18, 22],
],
[
[88, 88, 147, 150, 42, 46, 45, 196, 205],
[43, 97, 183, 117, 85, 38, 35, 179, 61],
[39, 53, 200, 87, 26, 21, 43, 232, 171],
[56, 34, 51, 104, 114, 102, 29, 93, 77],
[107, 54, 32, 26, 51, 1, 81, 43, 31],
[39, 28, 85, 171, 58, 165, 90, 98, 64],
[34, 22, 116, 206, 23, 34, 43, 166, 73],
[68, 25, 106, 22, 64, 171, 36, 225, 114],
[34, 19, 21, 102, 132, 188, 16, 76, 124],
[62, 18, 78, 95, 85, 57, 50, 48, 51],
],
[
[193, 101, 35, 159, 215, 111, 89, 46, 111],
[60, 148, 31, 172, 219, 228, 21, 18, 111],
[112, 113, 77, 85, 179, 255, 38, 120, 114],
[40, 42, 1, 196, 245, 209, 10, 25, 109],
[100, 80, 8, 43, 154, 1, 51, 26, 71],
[88, 43, 29, 140, 166, 213, 37, 43, 154],
[61, 63, 30, 155, 67, 45, 68, 1, 209],
[142, 78, 78, 16, 255, 128, 34, 197, 171],
[41, 40, 5, 102, 211, 183, 4, 1, 221],
[51, 50, 17, 168, 209, 192, 23, 25, 82],
],
[
[125, 98, 42, 88, 104, 85, 117, 175, 82],
[95, 84, 53, 89, 128, 100, 113, 101, 45],
[75, 79, 123, 47, 51, 128, 81, 171, 1],
[57, 17, 5, 71, 102, 57, 53, 41, 49],
[115, 21, 2, 10, 102, 255, 166, 23, 6],
[38, 33, 13, 121, 57, 73, 26, 1, 85],
[41, 10, 67, 138, 77, 110, 90, 47, 114],
[101, 29, 16, 10, 85, 128, 101, 196, 26],
[57, 18, 10, 102, 102, 213, 34, 20, 43],
[117, 20, 15, 36, 163, 128, 68, 1, 26],
],
[
[138, 31, 36, 171, 27, 166, 38, 44, 229],
[67, 87, 58, 169, 82, 115, 26, 59, 179],
[63, 59, 90, 180, 59, 166, 93, 73, 154],
[40, 40, 21, 116, 143, 209, 34, 39, 175],
[57, 46, 22, 24, 128, 1, 54, 17, 37],
[47, 15, 16, 183, 34, 223, 49, 45, 183],
[46, 17, 33, 183, 6, 98, 15, 32, 183],
[65, 32, 73, 115, 28, 128, 23, 128, 205],
[40, 3, 9, 115, 51, 192, 18, 6, 223],
[87, 37, 9, 115, 59, 77, 64, 21, 47],
],
[
[104, 55, 44, 218, 9, 54, 53, 130, 226],
[64, 90, 70, 205, 40, 41, 23, 26, 57],
[54, 57, 112, 184, 5, 41, 38, 166, 213],
[30, 34, 26, 133, 152, 116, 10, 32, 134],
[75, 32, 12, 51, 192, 255, 160, 43, 51],
[39, 19, 53, 221, 26, 114, 32, 73, 255],
[31, 9, 65, 234, 2, 15, 1, 118, 73],
[88, 31, 35, 67, 102, 85, 55, 186, 85],
[56, 21, 23, 111, 59, 205, 45, 37, 192],
[55, 38, 70, 124, 73, 102, 1, 34, 98],
],
[
[102, 61, 71, 37, 34, 53, 31, 243, 192],
[69, 60, 71, 38, 73, 119, 28, 222, 37],
[68, 45, 128, 34, 1, 47, 11, 245, 171],
[62, 17, 19, 70, 146, 85, 55, 62, 70],
[75, 15, 9, 9, 64, 255, 184, 119, 16],
[37, 43, 37, 154, 100, 163, 85, 160, 1],
[63, 9, 92, 136, 28, 64, 32, 201, 85],
[86, 6, 28, 5, 64, 255, 25, 248, 1],
[56, 8, 17, 132, 137, 255, 55, 116, 128],
[58, 15, 20, 82, 135, 57, 26, 121, 40],
],
[
[164, 50, 31, 137, 154, 133, 25, 35, 218],
[51, 103, 44, 131, 131, 123, 31, 6, 158],
[86, 40, 64, 135, 148, 224, 45, 183, 128],
[22, 26, 17, 131, 240, 154, 14, 1, 209],
[83, 12, 13, 54, 192, 255, 68, 47, 28],
[45, 16, 21, 91, 64, 222, 7, 1, 197],
[56, 21, 39, 155, 60, 138, 23, 102, 213],
[85, 26, 85, 85, 128, 128, 32, 146, 171],
[18, 11, 7, 63, 144, 171, 4, 4, 246],
[35, 27, 10, 146, 174, 171, 12, 26, 128],
],
[
[190, 80, 35, 99, 180, 80, 126, 54, 45],
[85, 126, 47, 87, 176, 51, 41, 20, 32],
[101, 75, 128, 139, 118, 146, 116, 128, 85],
[56, 41, 15, 176, 236, 85, 37, 9, 62],
[146, 36, 19, 30, 171, 255, 97, 27, 20],
[71, 30, 17, 119, 118, 255, 17, 18, 138],
[101, 38, 60, 138, 55, 70, 43, 26, 142],
[138, 45, 61, 62, 219, 1, 81, 188, 64],
[32, 41, 20, 117, 151, 142, 20, 21, 163],
[112, 19, 12, 61, 195, 128, 48, 4, 24],
],
];

View File

@ -0,0 +1,183 @@
//! The 4×4 transforms. The inverse DCT and inverse WHT are the bit-exact procedures of RFC 6386
//! §14.3 and §14.4 (the encoder reconstructs with them so its prediction matches the decoder's).
//! The forward transforms are their derived counterparts: the inverse DCT is twice the
//! orthonormal transform per dimension and then `>> 3`, so coefficients are twice the
//! orthonormal DCT of the residual; the inverse WHT is an unnormalized Hadamard pair `>> 3`, so
//! the Y2 input is half the Hadamard pair of the DC values.
/// `sqrt(2) * cos(pi / 8) - 1` and `sqrt(2) * sin(pi / 8)` in 16.16 fixed point (RFC 6386 §14.4).
const C1: i32 = 20091;
const S1: i32 = 35468;
/// Residual (16 values, row-major) → coefficients (row-major; the zig-zag is applied when coding).
pub fn fdct(input: &[i32; 16]) -> [i32; 16] {
let mut tmp = [0i32; 16];
// Columns first (so that the row pass mirrors the decoder's order of rounding steps).
for i in 0..4 {
let (x0, x1, x2, x3) = (input[i], input[4 + i], input[8 + i], input[12 + i]);
let a = x0 + x3;
let b = x1 + x2;
let c = x1 - x2;
let d = x0 - x3;
tmp[i] = a + b;
tmp[8 + i] = a - b;
// X1 = d * sqrt2cos(pi/8) + c * sqrt2sin(pi/8); X3 = d * sqrt2sin(pi/8) - c * sqrt2cos(pi/8).
tmp[4 + i] = ((d * (65536 + C1) + c * S1 + 32768) >> 16).clamp(-32768, 32767);
tmp[12 + i] = ((d * S1 - c * (65536 + C1) + 32768) >> 16).clamp(-32768, 32767);
}
let mut out = [0i32; 16];
for r in 0..4 {
let (x0, x1, x2, x3) = (tmp[r * 4], tmp[r * 4 + 1], tmp[r * 4 + 2], tmp[r * 4 + 3]);
let a = x0 + x3;
let b = x1 + x2;
let c = x1 - x2;
let d = x0 - x3;
// Two passes give 4× the orthonormal transform; the coefficients are 2× it.
out[r * 4] = (a + b + 1) >> 1;
out[r * 4 + 2] = (a - b + 1) >> 1;
out[r * 4 + 1] = ((d * (65536 + C1) + c * S1 + 65536) >> 17).clamp(-32768, 32767);
out[r * 4 + 3] = ((d * S1 - c * (65536 + C1) + 65536) >> 17).clamp(-32768, 32767);
}
out
}
/// Dequantized coefficients → residual, bit-exact with the decoder (RFC 6386 §14.4).
pub fn idct(input: &[i32; 16]) -> [i32; 16] {
let mut tmp = [0i32; 16];
for i in 0..4 {
let (ip0, ip4, ip8, ip12) = (input[i], input[4 + i], input[8 + i], input[12 + i]);
let a1 = ip0 + ip8;
let b1 = ip0 - ip8;
let temp1 = (ip4 * S1) >> 16;
let temp2 = ip12 + ((ip12 * C1) >> 16);
let c1 = temp1 - temp2;
let temp1 = ip4 + ((ip4 * C1) >> 16);
let temp2 = (ip12 * S1) >> 16;
let d1 = temp1 + temp2;
tmp[i] = a1 + d1;
tmp[12 + i] = a1 - d1;
tmp[4 + i] = b1 + c1;
tmp[8 + i] = b1 - c1;
}
let mut out = [0i32; 16];
for r in 0..4 {
let (ip0, ip1, ip2, ip3) = (tmp[r * 4], tmp[r * 4 + 1], tmp[r * 4 + 2], tmp[r * 4 + 3]);
let a1 = ip0 + ip2;
let b1 = ip0 - ip2;
let temp1 = (ip1 * S1) >> 16;
let temp2 = ip3 + ((ip3 * C1) >> 16);
let c1 = temp1 - temp2;
let temp1 = ip1 + ((ip1 * C1) >> 16);
let temp2 = (ip3 * S1) >> 16;
let d1 = temp1 + temp2;
out[r * 4] = (a1 + d1 + 4) >> 3;
out[r * 4 + 3] = (a1 - d1 + 4) >> 3;
out[r * 4 + 1] = (b1 + c1 + 4) >> 3;
out[r * 4 + 2] = (b1 - c1 + 4) >> 3;
}
out
}
/// The 16 luma DC values (row-major by sub-block) → Y2 coefficients.
pub fn fwht(input: &[i32; 16]) -> [i32; 16] {
let mut tmp = [0i32; 16];
for i in 0..4 {
let (x0, x1, x2, x3) = (input[i], input[4 + i], input[8 + i], input[12 + i]);
let a1 = x0 + x3;
let b1 = x1 + x2;
let c1 = x1 - x2;
let d1 = x0 - x3;
tmp[i] = a1 + b1;
tmp[4 + i] = c1 + d1;
tmp[8 + i] = a1 - b1;
tmp[12 + i] = d1 - c1;
}
let mut out = [0i32; 16];
for r in 0..4 {
let (x0, x1, x2, x3) = (tmp[r * 4], tmp[r * 4 + 1], tmp[r * 4 + 2], tmp[r * 4 + 3]);
let a1 = x0 + x3;
let b1 = x1 + x2;
let c1 = x1 - x2;
let d1 = x0 - x3;
// Half the unnormalized pair, rounded to nearest (ties away from zero).
let half = |v: i32| (v + v.signum()) / 2;
out[r * 4] = half(a1 + b1);
out[r * 4 + 1] = half(c1 + d1);
out[r * 4 + 2] = half(a1 - b1);
out[r * 4 + 3] = half(d1 - c1);
}
out
}
/// Dequantized Y2 coefficients → the 16 luma DC values, bit-exact with the decoder (RFC 6386 §14.3).
pub fn iwht(input: &[i32; 16]) -> [i32; 16] {
let mut tmp = [0i32; 16];
for i in 0..4 {
let a1 = input[i] + input[12 + i];
let b1 = input[4 + i] + input[8 + i];
let c1 = input[4 + i] - input[8 + i];
let d1 = input[i] - input[12 + i];
tmp[i] = a1 + b1;
tmp[4 + i] = c1 + d1;
tmp[8 + i] = a1 - b1;
tmp[12 + i] = d1 - c1;
}
let mut out = [0i32; 16];
for r in 0..4 {
let a1 = tmp[r * 4] + tmp[r * 4 + 3];
let b1 = tmp[r * 4 + 1] + tmp[r * 4 + 2];
let c1 = tmp[r * 4 + 1] - tmp[r * 4 + 2];
let d1 = tmp[r * 4] - tmp[r * 4 + 3];
out[r * 4] = (a1 + b1 + 3) >> 3;
out[r * 4 + 1] = (c1 + d1 + 3) >> 3;
out[r * 4 + 2] = (a1 - b1 + 3) >> 3;
out[r * 4 + 3] = (d1 - c1 + 3) >> 3;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn lcg(seed: &mut u64) -> i32 {
*seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
((*seed >> 33) % 511) as i32 - 255
}
#[test]
fn dct_inverts_within_rounding() {
let mut seed = 7;
for _ in 0..2000 {
let input: [i32; 16] = std::array::from_fn(|_| lcg(&mut seed));
let back = idct(&fdct(&input));
for (a, b) in input.iter().zip(&back) {
assert!((a - b).abs() <= 1, "{input:?} -> {back:?}");
}
}
}
#[test]
fn dct_of_a_constant_block_is_a_dc_only_block() {
let c = fdct(&[37; 16]);
assert_eq!(c[0], 37 * 8, "DC is 8× the mean (2× orthonormal, 4 × 4 samples)");
assert!(c[1..].iter().all(|&v| v == 0));
assert_eq!(idct(&c), [37; 16]);
}
#[test]
fn wht_inverts_exactly_in_the_decoders_range() {
let mut seed = 11;
for _ in 0..2000 {
let input: [i32; 16] = std::array::from_fn(|_| lcg(&mut seed) * 8);
let back = iwht(&fwht(&input));
for (a, b) in input.iter().zip(&back) {
assert!((a - b).abs() <= 1, "{input:?} -> {back:?}");
}
}
let c = fwht(&[80; 16]);
assert_eq!(c[0], 80 * 8);
assert!(c[1..].iter().all(|&v| v == 0));
assert_eq!(iwht(&c), [80; 16]);
}
}

View File

@ -1,5 +1,7 @@
//! WebP via `image-webp`: decode lossy/lossless (first frame), encode
//! lossless only (no pure-Rust lossy encoder exists). ICC/EXIF/XMP both ways.
//! WebP: decode lossy/lossless (first frame) via `image-webp`; encode lossless via
//! `image-webp`'s VP8L encoder, or lossy with our own VP8 encoder (`super::vp8`) wrapped in the
//! RIFF container written here (a bare `VP8 ` chunk, or `VP8X` with `ALPH` alpha and `ICCP` /
//! `EXIF` / `XMP ` chunks). ICC/EXIF/XMP both ways.
use std::io::Cursor;
@ -37,10 +39,10 @@ pub(crate) fn decode(bytes: &[u8], limits: &Limits) -> Result<Image, CodecError>
}
pub(crate) fn encode(src: &Image, plan: Plan, opts: &EncodeOptions) -> Result<Vec<u8>, CodecError> {
if !opts.webp_lossless {
return Err(CodecError::unsupported(F, "lossy WebP encoding needs libwebp (C); only lossless is available"));
}
let img = src.converted(plan.layout, plan.sample);
if !opts.webp_lossless {
return encode_lossy(&img, opts);
}
let ct = match img.layout() {
ChannelLayout::Gray => image_webp::ColorType::L8,
ChannelLayout::GrayA => image_webp::ColorType::La8,
@ -67,3 +69,88 @@ pub(crate) fn encode(src: &Image, plan: Plan, opts: &EncodeOptions) -> Result<Ve
enc.encode(img.data(), img.width(), img.height(), ct).map_err(|e| CodecError::encode(F, e))?;
Ok(out)
}
/// Lossy: the picture as a VP8 key frame, alpha (when any pixel is translucent) as an
/// uncompressed `ALPH` chunk, metadata as chunks.
fn encode_lossy(img: &Image, opts: &EncodeOptions) -> Result<Vec<u8>, CodecError> {
use super::vp8;
let (w, h) = img.dimensions();
if w > vp8::MAX_DIMENSION || h > vp8::MAX_DIMENSION {
return Err(CodecError::encode(F, format!("lossy WebP holds at most {} pixels per side", vp8::MAX_DIMENSION)));
}
// The VP8 path takes 8-bit RGB(A); gray becomes RGB with flat chroma.
let has_alpha = img.layout().has_alpha();
let target = if has_alpha { ChannelLayout::Rgba } else { ChannelLayout::Rgb };
let rgb = img.converted(target, SampleType::U8);
let stride = target.channels();
let yuv = vp8::Yuv::from_rgb(rgb.data(), w, h, stride);
let frame = vp8::encode_frame(&yuv, vp8::Params { quality: opts.webp_quality.min(100) }).map_err(|e| CodecError::encode(F, e))?;
let alpha: Option<Vec<u8>> = if has_alpha {
let a: Vec<u8> = rgb.data().iter().skip(3).step_by(4).copied().collect();
if a.iter().all(|&v| v == 255) { None } else { Some(a) }
} else {
None
};
let icc = if opts.embed_icc { img.icc.as_deref() } else { None };
let exif = if opts.embed_metadata { img.meta.exif.as_deref().map(crate::orientation::upright_exif) } else { None };
let xmp = if opts.embed_metadata { img.meta.xmp.as_deref().map(crate::orientation::upright_xmp) } else { None };
Ok(riff(w, h, &frame, alpha.as_deref(), icc, exif.as_deref(), xmp.as_deref().map(str::as_bytes)))
}
/// A RIFF chunk: FourCC, little-endian size, payload, padded to an even length.
fn chunk(out: &mut Vec<u8>, fourcc: &[u8; 4], payload: &[u8]) {
out.extend_from_slice(fourcc);
out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
out.extend_from_slice(payload);
if payload.len() % 2 == 1 {
out.push(0);
}
}
/// The WebP container around a VP8 frame: the simple form when there is nothing else, else the
/// extended form (`VP8X`) with the alpha plane and the metadata chunks in the specified order.
fn riff(w: u32, h: u32, frame: &[u8], alpha: Option<&[u8]>, icc: Option<&[u8]>, exif: Option<&[u8]>, xmp: Option<&[u8]>) -> Vec<u8> {
let mut body = b"WEBP".to_vec();
let extended = alpha.is_some() || icc.is_some() || exif.is_some() || xmp.is_some();
if extended {
let mut flags = 0u8;
if icc.is_some() {
flags |= 0x20;
}
if alpha.is_some() {
flags |= 0x10;
}
if exif.is_some() {
flags |= 0x08;
}
if xmp.is_some() {
flags |= 0x04;
}
let mut vp8x = vec![flags, 0, 0, 0];
vp8x.extend_from_slice(&(w - 1).to_le_bytes()[..3]);
vp8x.extend_from_slice(&(h - 1).to_le_bytes()[..3]);
chunk(&mut body, b"VP8X", &vp8x);
if let Some(icc) = icc {
chunk(&mut body, b"ICCP", icc);
}
if let Some(a) = alpha {
// Header byte: reserved, no pre-processing, no filtering, no compression.
let mut alph = Vec::with_capacity(a.len() + 1);
alph.push(0);
alph.extend_from_slice(a);
chunk(&mut body, b"ALPH", &alph);
}
}
chunk(&mut body, b"VP8 ", frame);
if let Some(exif) = exif {
chunk(&mut body, b"EXIF", exif);
}
if let Some(xmp) = xmp {
chunk(&mut body, b"XMP ", xmp);
}
let mut out = Vec::with_capacity(body.len() + 8);
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}

View File

@ -210,7 +210,7 @@ pub fn fidelity_warnings_with(image: &Image, format: Format, opts: &EncodeOption
// Compression
if format == Format::Gif {
w.push(W::PaletteQuantized);
} else if c.lossy {
} else if c.lossy || (format == Format::WebP && !opts.webp_lossless) {
w.push(W::LossyCompression);
}

View File

@ -114,9 +114,11 @@ pub struct EncodeOptions {
pub png_compression: PngCompression,
/// Write Adam7-interlaced PNG.
pub png_interlaced: bool,
/// WebP: only lossless encoding is available in pure Rust; `false`
/// yields [`CodecError::Unsupported`].
/// WebP: lossless (VP8L, via `image-webp`) or lossy (our VP8 encoder, see
/// `codecs::vp8`) with [`Self::webp_quality`].
pub webp_lossless: bool,
/// Lossy WebP quality 0..=100 (the source application's scale; 75–85 is typical for photos).
pub webp_quality: u8,
pub tiff_compression: TiffCompression,
pub exr_compression: ExrCompression,
/// Embed the ICC profile when the format supports it.
@ -133,6 +135,7 @@ impl Default for EncodeOptions {
png_compression: PngCompression::Default,
png_interlaced: false,
webp_lossless: true,
webp_quality: 80,
tiff_compression: TiffCompression::Deflate,
exr_compression: ExrCompression::Zip16,
embed_icc: true,

View File

@ -226,10 +226,15 @@ fn jpeg_444_beats_420_on_chroma_detail() {
}
#[test]
fn webp_lossy_request_is_honest_error() {
fn webp_lossy_request_is_a_lossy_file() {
let img = test_image(ChannelLayout::Rgb, SampleType::U8);
let e = encode(&img, Format::WebP, &EncodeOptions { webp_lossless: false, ..Default::default() }).unwrap_err();
assert!(matches!(e, CodecError::Unsupported { format: Format::WebP, .. }));
let bytes = encode(&img, Format::WebP, &EncodeOptions { webp_lossless: false, ..Default::default() }).unwrap();
assert_eq!(&bytes[12..16], b"VP8 ", "a bare lossy frame");
let back = decode(&bytes).unwrap();
assert_eq!(back.dimensions(), img.dimensions());
assert!(
fidelity_warnings_with(&img, Format::WebP, &EncodeOptions { webp_lossless: false, ..Default::default() }).contains(&FidelityWarning::LossyCompression)
);
}
#[test]

View File

@ -0,0 +1,140 @@
//! Lossy WebP (our VP8 encoder) checked against an independent decoder (`image-webp`): the
//! files decode, look like the input (PSNR by quality), get smaller as quality drops and are far
//! smaller than lossless on photographic content; alpha and metadata travel in the container;
//! odd sizes and the limits behave.
mod common;
use common::*;
use photocraft_codecs::*;
fn lossy(q: u8) -> EncodeOptions {
EncodeOptions { webp_lossless: false, webp_quality: q, ..Default::default() }
}
/// A smooth photograph-like image: gradients with a little grain.
fn photo(w: u32, h: u32, seed: u64) -> Image {
synth(w, h, ChannelLayout::Rgb, SampleType::U8, seed, 0.03)
}
#[test]
fn decodes_with_the_right_size_and_looks_like_the_input() {
let img = photo(97, 61, 3);
for (q, min_psnr) in [(100u8, 40.0), (90, 36.0), (75, 33.0), (50, 30.0), (20, 26.0)] {
let bytes = encode(&img, Format::WebP, &lossy(q)).unwrap();
let back = decode(&bytes).unwrap_or_else(|e| panic!("q{q}: {e}"));
assert_eq!(back.dimensions(), (97, 61));
assert_eq!(back.layout(), ChannelLayout::Rgb);
let p = psnr(&img, &back);
assert!(p >= min_psnr, "quality {q}: PSNR {p:.1} dB below {min_psnr}");
}
}
#[test]
fn smaller_at_lower_quality_and_much_smaller_than_lossless_on_photos() {
let img = synth(256, 192, ChannelLayout::Rgb, SampleType::U8, 9, 0.08);
let lossless = encode(&img, Format::WebP, &EncodeOptions::default()).unwrap().len();
let sizes: Vec<usize> = [95u8, 80, 60, 40, 20].iter().map(|&q| encode(&img, Format::WebP, &lossy(q)).unwrap().len()).collect();
for w in sizes.windows(2) {
assert!(w[1] <= w[0], "sizes must not grow as quality drops: {sizes:?}");
}
assert!(sizes[1] * 4 < lossless, "q80 {} vs lossless {lossless}", sizes[1]);
assert!(sizes[4] * 10 < lossless, "q20 {} vs lossless {lossless}", sizes[4]);
}
#[test]
fn flat_areas_cost_almost_nothing() {
// A single colour: every macroblock is skipped; the file is dominated by the header.
let img = Image::from_u8(640, 480, ChannelLayout::Rgb, vec![200u8; 640 * 480 * 3]).unwrap();
let bytes = encode(&img, Format::WebP, &lossy(80)).unwrap();
assert!(bytes.len() < 1200, "{} bytes", bytes.len());
let back = decode(&bytes).unwrap();
assert!(max_abs_diff(&img, &back) <= 3.0 / 255.0, "flat colour reproduced within rounding");
}
#[test]
fn alpha_is_kept_exactly_and_metadata_travels() {
let mut img = synth(40, 30, ChannelLayout::Rgba, SampleType::U8, 5, 0.05);
img.icc = Some(sample_icc(400));
img.meta.exif = Some(sample_exif());
img.meta.xmp = Some(SAMPLE_XMP.to_owned());
let bytes = encode(&img, Format::WebP, &lossy(85)).unwrap();
assert_eq!(&bytes[12..16], b"VP8X", "extended container");
let back = decode(&bytes).unwrap();
assert_eq!(back.layout(), ChannelLayout::Rgba);
for (a, b) in img.data().as_chunks::<4>().0.iter().zip(back.data().as_chunks::<4>().0) {
assert_eq!(a[3], b[3], "alpha is stored losslessly");
}
assert!(psnr(&img, &back) > 30.0);
assert_eq!(back.icc, img.icc);
assert!(back.meta.xmp.is_some());
assert!(back.meta.exif.is_some());
// Fully opaque alpha is dropped rather than stored.
let mut data = synth(40, 30, ChannelLayout::Rgba, SampleType::U8, 5, 0.0).data().to_vec();
for px in data.as_chunks_mut::<4>().0 {
px[3] = 255;
}
let opaque = Image::from_u8(40, 30, ChannelLayout::Rgba, data).unwrap();
let bytes = encode(&opaque, Format::WebP, &EncodeOptions { embed_icc: false, embed_metadata: false, ..lossy(85) }).unwrap();
assert_eq!(&bytes[12..16], b"VP8 ");
assert_eq!(decode(&bytes).unwrap().layout(), ChannelLayout::Rgb);
}
#[test]
fn gray_and_odd_sizes_and_tiny_images() {
for (w, h) in [(1, 1), (2, 3), (17, 9), (33, 1), (1, 47), (16, 16), (31, 15)] {
for layout in [ChannelLayout::Gray, ChannelLayout::GrayA, ChannelLayout::Rgb, ChannelLayout::Rgba] {
// Little noise: at these sizes the 2 × 2 chroma average alone would otherwise dominate.
let img = synth(w, h, layout, SampleType::U8, u64::from(w * 100 + h), 0.02);
let bytes = encode(&img, Format::WebP, &lossy(80)).unwrap_or_else(|e| panic!("{w}x{h} {layout:?}: {e}"));
let back = decode(&bytes).unwrap_or_else(|e| panic!("{w}x{h} {layout:?}: {e}"));
assert_eq!(back.dimensions(), (w, h));
// Below 8 px the 2 × 2 chroma average of random saturated pixels is the whole error.
if w >= 8 && h >= 8 {
let p = psnr(&img.convert(back.layout(), SampleType::U8), &back);
assert!(p > 24.0, "{w}x{h} {layout:?}: {p:.1} dB");
}
}
}
}
#[test]
fn sixteen_bit_input_is_quantised_to_eight() {
let img = synth(24, 24, ChannelLayout::Rgb, SampleType::U16, 2, 0.02);
let bytes = encode(&img, Format::WebP, &lossy(90)).unwrap();
let back = decode(&bytes).unwrap();
assert!(psnr(&img.convert(ChannelLayout::Rgb, SampleType::U8), &back) > 34.0);
}
#[test]
fn dimension_limit_is_an_error_not_a_panic() {
let img = Image::from_u8(16384, 1, ChannelLayout::Rgb, vec![0u8; 16384 * 3]).unwrap();
let e = encode(&img, Format::WebP, &lossy(50)).unwrap_err();
assert!(matches!(e, CodecError::Encode { .. }), "{e}");
let img = Image::from_u8(16383, 1, ChannelLayout::Rgb, vec![0u8; 16383 * 3]).unwrap();
let bytes = encode(&img, Format::WebP, &lossy(50)).unwrap();
assert_eq!(decode(&bytes).unwrap().dimensions(), (16383, 1));
}
#[test]
fn hard_edges_and_text_like_content_keep_their_structure() {
// Black text-like strokes on white: B_PRED sub-block modes keep edges sharp enough that the
// thresholded result matches the source almost everywhere.
let (w, h) = (128u32, 64u32);
let mut data = vec![255u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let stroke = (x % 11 < 2 && y > 8 && y < 56) || (y % 13 < 2 && x > 4 && x < 124);
if stroke {
let i = ((y * w + x) * 3) as usize;
data[i..i + 3].fill(0);
}
}
}
let img = Image::from_u8(w, h, ChannelLayout::Rgb, data).unwrap();
let bytes = encode(&img, Format::WebP, &lossy(90)).unwrap();
let back = decode(&bytes).unwrap();
let wrong = img.data().as_chunks::<3>().0.iter().zip(back.data().as_chunks::<3>().0).filter(|(a, b)| (a[0] < 128) != (b[0] < 128)).count();
let total = (w * h) as usize;
assert!(wrong * 100 < total * 2, "{wrong} of {total} pixels flipped side of mid-gray");
assert!(psnr(&img, &back) > 30.0, "{:.1}", psnr(&img, &back));
}

View File

@ -213,7 +213,11 @@ pub(crate) fn encode(doc: &Document, path: &str, save: impl Into<SaveOpts>) -> R
let save = save.into();
let mut opts = photocraft_io::ExportOptions { tiff_layers: save.tiff_layers, ..Default::default() };
if let Some(q) = save.quality {
opts.encode.jpeg_quality = (q.clamp(0.0, 12.0) / 12.0 * 99.0 + 1.0).round() as u8;
let q = (q.clamp(0.0, 12.0) / 12.0 * 99.0 + 1.0).round() as u8;
opts.encode.jpeg_quality = q;
// A quality on a WebP save asks for the lossy encoder; the default WebP stays lossless.
opts.encode.webp_quality = q;
opts.encode.webp_lossless = false;
}
photocraft_io::export(doc, path, &opts).map(|r| (r.bytes, r.warnings)).map_err(|e| EngineError::Other(format!("{path}: {e}")))
}

View File

@ -44,6 +44,10 @@ fn main() {
if let Some(q) = settings.jpeg_quality {
opts.encode.jpeg_quality = q;
}
opts.encode.webp_lossless = settings.webp_lossless;
if let Some(q) = settings.webp_quality {
opts.encode.webp_quality = q;
}
photocraft_io::export(doc, path, &opts).map(|r| (r.bytes, r.warnings)).map_err(|e| e.to_string())
})),
write: Some(Box::new(|path: &str, bytes: &[u8]| photocraft_format::atomic_write(std::path::Path::new(path), bytes).map_err(|e| e.to_string()))),

View File

@ -11,7 +11,7 @@ use crate::state::DialogKind;
use crate::theme::Tokens;
use crate::{ExportSettings, PhotocraftApp};
const FORMATS: [(&str, &str); 5] = [("png", "PNG"), ("jpg", "JPG"), ("webp", "WebP (lossless)"), ("tif", "TIFF"), ("tga", "TGA")];
const FORMATS: [(&str, &str); 5] = [("png", "PNG"), ("jpg", "JPG"), ("webp", "WebP"), ("tif", "TIFF"), ("tga", "TGA")];
pub fn open(app: &mut PhotocraftApp) -> Result<u64, String> {
let st = app.session.active().ok_or("no document")?;
@ -20,6 +20,7 @@ pub fn open(app: &mut PhotocraftApp) -> Result<u64, String> {
f.insert("__label".into(), json!("Export As"));
f.insert("format".into(), json!("png"));
f.insert("quality".into(), json!(85));
f.insert("lossless".into(), json!(false));
f.insert("transparency".into(), json!(true));
f.insert("scale".into(), json!(100));
f.insert("metadata".into(), json!("none"));
@ -96,9 +97,17 @@ fn s_fmt(f: &Map<String, Value>) -> String {
if v.is_empty() { "png".into() } else { v }
}
fn lossless(f: &Map<String, Value>) -> bool {
f.get("lossless").and_then(Value::as_bool).unwrap_or(false)
}
fn settings(f: &Map<String, Value>) -> ExportSettings {
let fmt = s_fmt(f);
let quality = n(f, "quality", 85.0).clamp(1.0, 100.0) as u8;
ExportSettings {
jpeg_quality: (s_fmt(f) == "jpg").then(|| n(f, "quality", 85.0).clamp(1.0, 100.0) as u8),
jpeg_quality: (fmt == "jpg").then_some(quality),
webp_lossless: fmt != "webp" || lossless(f),
webp_quality: (fmt == "webp" && !lossless(f)).then_some(quality),
xmp_all: s(f, "metadata") == "all",
..Default::default()
}
@ -132,11 +141,17 @@ pub fn body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Valu
}
});
let fmt = s_fmt(f);
if fmt == "jpg" {
if fmt == "webp" {
let mut ll = lossless(f);
crate::widgets::checkbox(ui, &mut ll, tl!("Lossless"));
f.insert("lossless".into(), json!(ll));
}
if fmt == "jpg" || (fmt == "webp" && !lossless(f)) {
let mut q = n(f, "quality", 85.0) as f32;
crate::widgets::slider_row(ui, tl!("Quality"), &mut q, 1.0..=100.0, "%", None);
f.insert("quality".into(), json!(q.round()));
} else {
}
if fmt != "jpg" {
let mut tr = f.get("transparency").and_then(Value::as_bool).unwrap_or(true);
crate::widgets::checkbox(ui, &mut tr, tl!("Transparency"));
f.insert("transparency".into(), json!(tr));
@ -167,9 +182,10 @@ pub fn body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Valu
let layer = f.get("__layer").and_then(Value::as_u64);
let key = egui::Id::new(("export-preview", doc.id.0, app.session.active().map_or(0, |s| s.revision), layer));
let sig = format!(
"{}{}{}{}{}",
"{}{}{}{}{}{}",
s_fmt(f),
n(f, "quality", 85.0),
lossless(f),
f.get("transparency").map(|v| v.to_string()).unwrap_or_default(),
n(f, "scale", 100.0),
s(f, "metadata")
@ -270,4 +286,21 @@ mod tests {
assert_eq!(proxy.size.width, 40);
assert_eq!(settings(&f).jpeg_quality, Some(85));
}
#[test]
fn webp_settings_follow_the_lossless_switch() {
let mut f = Map::new();
f.insert("format".into(), json!("webp"));
f.insert("quality".into(), json!(70));
let s = settings(&f);
assert!(!s.webp_lossless, "Export As writes lossy WebP unless asked");
assert_eq!(s.webp_quality, Some(70));
assert_eq!(s.jpeg_quality, None);
f.insert("lossless".into(), json!(true));
let s = settings(&f);
assert!(s.webp_lossless);
assert_eq!(s.webp_quality, None);
f.insert("format".into(), json!("png"));
assert!(settings(&f).webp_lossless, "other formats leave the WebP default alone");
}
}

View File

@ -1802,6 +1802,7 @@
Light gray Světle šedá
Line Čára
Lines Čáry
Lossless Bezeztrátově
Lowercase extension Přípona malými písmeny
Maximize psd compatibility Maximalizovat kompatibilitu PSD
Medium gray Středně šedá

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -1780,6 +1780,7 @@
Light gray Hellgrau
Line Linie
Lines Linien
Lossless Verlustfrei
Lowercase extension Dateinamenserweiterung in Kleinschreibung
Maximize psd compatibility PSD-Kompatibilität maximieren
Medium gray Mittelgrau

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -1789,6 +1789,7 @@
Light gray Gris claro
Line Línea
Lines Líneas
Lossless Sin pérdida
Lowercase extension Extensión en minúsculas
Maximize psd compatibility Maximizar la compatibilidad de PSD
Medium gray Gris medio

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -1767,6 +1767,7 @@
Light gray Gris clair
Line Ligne
Lines Lignes
Lossless Sans perte
Lowercase extension Extension en minuscules
Maximize psd compatibility Maximiser la compatibilité PSD
Medium gray Gris moyen

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -1761,6 +1761,7 @@
Light gray Abu-abu terang
Line Garis
Lines Garis
Lossless Tanpa kehilangan
Lowercase extension Ekstensi huruf kecil
Maximize psd compatibility Maksimalkan kompatibilitas PSD
Medium gray Abu-abu sedang

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -1787,6 +1787,7 @@
Light gray Grigio chiaro
Line Linea
Lines Linee
Lossless Senza perdita
Lowercase extension Estensione minuscola
Maximize psd compatibility Massimizza compatibilità PSD
Medium gray Grigio medio

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -1794,6 +1794,7 @@
Light gray ライトグレー
Line 線
Lines 線
Lossless 可逆圧縮
Lowercase extension 拡張子を小文字にする
Maximize psd compatibility PSD の互換性を最大にする
Medium gray 中間グレー

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -1762,6 +1762,7 @@
Light gray 밝은 회색
Line 선
Lines 선
Lossless 무손실
Lowercase extension 소문자 확장자
Maximize psd compatibility PSD 호환성 최대화
Medium gray 중간 회색

Can't render this file because it has a wrong number of fields in line 10.

View File

@ -1766,6 +1766,7 @@
Light gray Cinza-claro
Line Linha
Lines Linhas
Lossless Sem perdas
Lowercase extension Extensão em minúsculas
Maximize psd compatibility Maximizar compatibilidade de PSD
Medium gray Cinza médio

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -1233,6 +1233,7 @@
Urgency Срочность
High Высокая
Normal Обычная
Lossless Без потерь
Low Низкая
Category Категория
Supplemental Categories Дополнительные категории

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -1796,6 +1796,7 @@
Light gray 浅灰色
Line 线条
Lines 线条
Lossless 无损
Lowercase extension 小写扩展名
Maximize psd compatibility 最大化 PSD 兼容性
Medium gray 中灰色

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -1789,6 +1789,7 @@
Light gray 淺灰
Line 直線
Lines 直線
Lossless 無損
Lowercase extension 副檔名小寫
Maximize psd compatibility 最大化 PSD 相容性
Medium gray 中灰

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -153,6 +153,10 @@ pub type ImportFn = Box<dyn Fn(&str, &[u8]) -> Result<(Document, Vec<String>), S
pub struct ExportSettings {
/// JPEG quality 1–100 (None = codec default).
pub jpeg_quality: Option<u8>,
/// WebP: lossless (VP8L) rather than lossy (VP8 at [`Self::webp_quality`]).
pub webp_lossless: bool,
/// Lossy WebP quality 1–100 (None = codec default).
pub webp_quality: Option<u8>,
/// TIFF: keep the layers (Photoshop layer data); `false` is "Discard Layers and Save a Copy".
pub tiff_layers: bool,
/// Embed the document's whole XMP packet. `true` by default (Save As keeps the metadata);
@ -163,7 +167,7 @@ pub struct ExportSettings {
impl Default for ExportSettings {
fn default() -> Self {
ExportSettings { jpeg_quality: None, tiff_layers: true, xmp_all: true }
ExportSettings { jpeg_quality: None, webp_lossless: true, webp_quality: None, tiff_layers: true, xmp_all: true }
}
}