#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, allow(unused_attributes))]
#![allow(clippy::new_without_default)]
#![deny(missing_docs)]
#![deny(unsafe_code)]
#[cfg(all(feature = "alloc", not(feature = "std")))]
extern crate alloc as std;
#[cfg(feature = "std")]
extern crate std;
#[cfg(any(feature = "alloc", feature = "std"))]
use std::vec::Vec;
pub use colorthief_dataset::{Algorithm, Color, Family, Kind};
mod buffer;
pub use buffer::Buffer;
mod mmcq;
pub use mmcq::Mmcq;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Dominant {
pub(crate) rgb: [u8; 3],
pub(crate) color: &'static Color,
pub(crate) population: u32,
pub(crate) percentage: f32,
}
impl Dominant {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn rgb(&self) -> [u8; 3] {
self.rgb
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn color(&self) -> &'static Color {
self.color
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn population(&self) -> u32 {
self.population
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn percentage(&self) -> f32 {
self.percentage
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)]
#[non_exhaustive]
pub enum RgbFrameError {
#[error("width ({width}) or height ({height}) is zero")]
ZeroDimension {
width: u32,
height: u32,
},
#[error("stride ({stride}) is smaller than 3 * width ({min_stride})")]
StrideTooSmall {
min_stride: u32,
stride: u32,
},
#[error("RGB plane has {actual} bytes but at least {expected} are required")]
PlaneTooShort {
expected: usize,
actual: usize,
},
#[error("declared geometry overflows usize: stride={stride} * rows={rows}")]
GeometryOverflow {
stride: u32,
rows: u32,
},
#[error("3 * width overflows u32 ({width} too large)")]
WidthOverflow {
width: u32,
},
}
#[derive(Debug, Clone, Copy)]
pub struct RgbFrame<'a> {
rgb: &'a [u8],
width: u32,
height: u32,
stride: u32,
}
impl<'a> RgbFrame<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn try_new(
rgb: &'a [u8],
width: u32,
height: u32,
stride: u32,
) -> Result<Self, RgbFrameError> {
if width == 0 || height == 0 {
return Err(RgbFrameError::ZeroDimension { width, height });
}
let min_stride = match width.checked_mul(3) {
Some(v) => v,
None => return Err(RgbFrameError::WidthOverflow { width }),
};
if stride < min_stride {
return Err(RgbFrameError::StrideTooSmall { min_stride, stride });
}
let plane_min = match (stride as usize).checked_mul(height as usize) {
Some(v) => v,
None => {
return Err(RgbFrameError::GeometryOverflow {
stride,
rows: height,
});
}
};
if rgb.len() < plane_min {
return Err(RgbFrameError::PlaneTooShort {
expected: plane_min,
actual: rgb.len(),
});
}
Ok(Self {
rgb,
width,
height,
stride,
})
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn rgb(&self) -> &'a [u8] {
self.rgb
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn width(&self) -> u32 {
self.width
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn height(&self) -> u32 {
self.height
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn stride(&self) -> u32 {
self.stride
}
pub fn pixels(&self) -> impl Iterator<Item = [u8; 3]> + '_ {
let row_bytes = self.width as usize * 3;
let stride = self.stride as usize;
(0..self.height as usize).flat_map(move |row| {
let start = row * stride;
self.rgb[start..start + row_bytes]
.chunks_exact(3)
.map(|c| [c[0], c[1], c[2]])
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct Rgb48Frame<'a> {
rgb16: &'a [u16],
width: u32,
height: u32,
stride: u32,
}
impl<'a> Rgb48Frame<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn try_new(
rgb16: &'a [u16],
width: u32,
height: u32,
stride: u32,
) -> Result<Self, RgbFrameError> {
if width == 0 || height == 0 {
return Err(RgbFrameError::ZeroDimension { width, height });
}
let min_stride = match width.checked_mul(3) {
Some(v) => v,
None => return Err(RgbFrameError::WidthOverflow { width }),
};
if stride < min_stride {
return Err(RgbFrameError::StrideTooSmall { min_stride, stride });
}
let plane_min = match (stride as usize).checked_mul(height as usize) {
Some(v) => v,
None => {
return Err(RgbFrameError::GeometryOverflow {
stride,
rows: height,
});
}
};
if rgb16.len() < plane_min {
return Err(RgbFrameError::PlaneTooShort {
expected: plane_min,
actual: rgb16.len(),
});
}
Ok(Self {
rgb16,
width,
height,
stride,
})
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn rgb16(&self) -> &'a [u16] {
self.rgb16
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn width(&self) -> u32 {
self.width
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn height(&self) -> u32 {
self.height
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn stride(&self) -> u32 {
self.stride
}
pub fn pixels(&self) -> impl Iterator<Item = [u8; 3]> + '_ {
let row_u16 = self.width as usize * 3;
let stride_u16 = self.stride as usize;
(0..self.height as usize).flat_map(move |row| {
let start = row * stride_u16;
self.rgb16[start..start + row_u16]
.chunks_exact(3)
.map(|p| [(p[0] >> 8) as u8, (p[1] >> 8) as u8, (p[2] >> 8) as u8])
})
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
#[inline]
pub fn extract(frame: RgbFrame<'_>, count: u8) -> Vec<Dominant> {
extract_with(frame, count, Algorithm::default())
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
pub fn extract_with(frame: RgbFrame<'_>, count: u8, algo: Algorithm) -> Vec<Dominant> {
if count == 0 {
return Vec::new();
}
extract_dominants_from_pixels(frame.pixels(), count, algo)
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
#[inline]
pub fn extract_rgb48(frame: Rgb48Frame<'_>, count: u8) -> Vec<Dominant> {
extract_rgb48_with(frame, count, Algorithm::default())
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
pub fn extract_rgb48_with(frame: Rgb48Frame<'_>, count: u8, algo: Algorithm) -> Vec<Dominant> {
if count == 0 {
return Vec::new();
}
extract_dominants_from_pixels(frame.pixels(), count, algo)
}
#[cfg(any(feature = "alloc", feature = "std"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))]
fn extract_dominants_from_pixels<I: Iterator<Item = [u8; 3]>>(
pixels: I,
count: u8,
algo: Algorithm,
) -> Vec<Dominant> {
mmcq::quantize(pixels, count, algo)
}