///|
/// Palette strategy used by still-image and animation export paths.
pub(all) enum PalettePlanMode {
LocalMedianCut
SharedMedianCut
OrderedShared
FloydShared
} derive(Eq, Debug)
///|
pub(all) struct PalettePlan {
mode : PalettePlanMode
max_colors : Int
transparent : Rgba?
} derive(Debug)
///|
pub fn PalettePlan::new(
mode : PalettePlanMode,
max_colors : Int,
) -> PalettePlan {
{
mode,
max_colors: if max_colors < 2 {
2
} else if max_colors > 256 {
256
} else {
max_colors
},
transparent: None,
}
}
///|
pub fn PalettePlan::with_transparent(
self : PalettePlan,
color : Rgba?,
) -> PalettePlan {
{ ..self, transparent: color }
}
///|
pub fn PalettePlan::colors(self : PalettePlan) -> Int {
self.max_colors
}
///|
pub fn PalettePlan::mode(self : PalettePlan) -> PalettePlanMode {
self.mode
}
///|
pub(all) struct PaletteQuality {
palette_size : Int
pixels : Int
mean_error : Int
maximum_error : Int
exact_pixels : Int
} derive(Debug)
///|
pub fn PaletteQuality::palette_size(self : PaletteQuality) -> Int {
self.palette_size
}
///|
pub fn PaletteQuality::pixels(self : PaletteQuality) -> Int {
self.pixels
}
///|
pub fn PaletteQuality::mean_error(self : PaletteQuality) -> Int {
self.mean_error
}
///|
pub fn PaletteQuality::maximum_error(self : PaletteQuality) -> Int {
self.maximum_error
}
///|
pub fn PaletteQuality::exact_pixels(self : PaletteQuality) -> Int {
self.exact_pixels
}
///|
fn color_distance_squared(left : Rgba, right : Rgba) -> Int {
let dr = left.red().to_int() - right.red().to_int()
let dg = left.green().to_int() - right.green().to_int()
let db = left.blue().to_int() - right.blue().to_int()
let da = left.alpha().to_int() - right.alpha().to_int()
dr * dr + dg * dg + db * db + da * da
}
///|
/// Return the closest palette entry using squared Euclidean RGBA distance.
pub fn Palette::nearest_rgba(self : Palette, color : Rgba) -> Int {
let mut best = 0
let mut score = 1 << 30
for i, candidate in self.colors {
let distance = color_distance_squared(color, candidate)
if distance < score {
best = i
score = distance
}
}
best
}
///|
/// Remap pixels to a supplied palette without changing canvas dimensions.
pub fn remap_to_palette(canvas : Canvas, palette : Palette) -> QuantizedFrame {
let indices : Array[Byte] = []
for color in canvas.pixels {
indices.push(palette.nearest_rgba(color).to_byte())
}
QuantizedFrame::{ palette, indices }
}
///|
/// Reconstruct a canvas from a quantized frame.
pub fn QuantizedFrame::to_canvas(
self : QuantizedFrame,
width~ : Int,
height~ : Int,
) -> Canvas {
let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
let palette = self.palette()
for i in 0.. PaletteQuality {
let palette = quantized.palette()
let mut total = 0
let mut maximum = 0
let mut exact = 0
for i in 0.. maximum {
maximum = root
}
if root == 0 {
exact += 1
}
}
PaletteQuality::{
palette_size: palette.len(),
pixels: canvas.pixels.length(),
mean_error: if canvas.pixels.length() == 0 {
0
} else {
total / canvas.pixels.length()
},
maximum_error: maximum,
exact_pixels: exact,
}
}
///|
fn plan_one(
canvas : Canvas,
plan : PalettePlan,
shared : Palette?,
) -> QuantizedFrame {
match plan.mode {
LocalMedianCut =>
match plan.transparent {
None => quantize(canvas, max_colors=plan.max_colors)
Some(color) =>
quantize(canvas, max_colors=plan.max_colors, transparent=color)
}
SharedMedianCut =>
match shared {
None => quantize(canvas, max_colors=plan.max_colors)
Some(palette) => remap_to_palette(canvas, palette)
}
OrderedShared =>
match shared {
None =>
quantize_dithered(
canvas,
plan.max_colors,
plan.transparent,
Ordered2x2,
)
Some(palette) => remap_to_palette(canvas, palette)
}
FloydShared =>
match shared {
None =>
quantize_dithered(
canvas,
plan.max_colors,
plan.transparent,
FloydSteinberg,
)
Some(palette) => remap_to_palette(canvas, palette)
}
}
}
///|
fn shared_palette(animation : Animation, plan : PalettePlan) -> Palette? {
if animation.frames.length() == 0 {
None
} else {
let samples : Array[Rgba] = []
for frame in animation.frames {
let canvas = frame.canvas()
let stride = if canvas.len() > 4096 { canvas.len() / 4096 } else { 1 }
let mut i = 0
while i < canvas.len() {
samples.push(canvas.pixels[i])
i += stride
}
}
let width = if samples.length() > 0 { samples.length() } else { 1 }
let sample_canvas = Canvas::{ width, height: 1, pixels: samples }
Some(
match plan.transparent {
None => quantize(sample_canvas, max_colors=plan.max_colors).palette()
Some(color) =>
quantize(sample_canvas, max_colors=plan.max_colors, transparent=color).palette()
},
)
}
}
///|
/// Quantize every frame while making the palette-sharing decision explicit.
pub fn quantize_animation(
animation : Animation,
plan : PalettePlan,
) -> Array[QuantizedFrame] {
let shared = match plan.mode {
LocalMedianCut => None
_ => shared_palette(animation, plan)
}
let result : Array[QuantizedFrame] = []
for frame in animation.frames {
result.push(plan_one(frame.canvas(), plan, shared))
}
result
}
///|
/// Return per-frame palette distortion for export reports.
pub fn quantify_animation(
animation : Animation,
plan : PalettePlan,
) -> Array[PaletteQuality] {
let quantized = quantize_animation(animation, plan)
let result : Array[PaletteQuality] = []
for i in 0.. Palette {
let safe_size = if size < 2 { 2 } else if size > 256 { 256 } else { size }
let colors : Array[Rgba] = []
for i in 0.. Palette {
let safe_size = if size < 2 { 2 } else if size > 256 { 256 } else { size }
let colors : Array[Rgba] = []
for i in 0.. Palette {
match self.transparent_index() {
Some(_) => self
None => {
let colors : Array[Rgba] = []
for candidate in self.colors {
colors.push(candidate)
}
colors.push(color.with_alpha(0))
Palette::new(colors~, transparent_index=Some(colors.length() - 1))
}
}
}
///|
/// Report whether a quantized frame obeys GIF's 8-bit palette contract.
pub fn QuantizedFrame::is_gif_compatible(self : QuantizedFrame) -> Bool {
self.palette().len() > 0 &&
self.palette().len() <= 256 &&
self.indices.length() >= 0
}
///|
/// Clamp an arbitrary palette budget to GIF's legal range.
pub fn clamp_palette_size(size : Int) -> Int {
if size < 2 {
2
} else if size > 256 {
256
} else {
size
}
}