use std::fmt;
use std::str::FromStr;
use crate::{Error, Limits, Result};
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Color {
pub const TRANSPARENT: Color = Color::rgba(0, 0, 0, 0);
pub const BLACK: Color = Color::rgb(0, 0, 0);
pub const WHITE: Color = Color::rgb(255, 255, 255);
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, a: 255 }
}
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub const fn to_array(self) -> [u8; 4] {
[self.r, self.g, self.b, self.a]
}
pub const fn from_array([r, g, b, a]: [u8; 4]) -> Self {
Self { r, g, b, a }
}
pub fn over(self, below: Color) -> Color {
if self.a == 255 || below.a == 0 {
return self;
}
if self.a == 0 {
return below;
}
let sa = self.a as u32;
let da = below.a as u32 * (255 - sa);
let out = sa * 255 + da;
let channel = |s: u8, d: u8| ((s as u32 * sa * 255 + d as u32 * da + out / 2) / out) as u8;
Color {
r: channel(self.r, below.r),
g: channel(self.g, below.g),
b: channel(self.b, below.b),
a: ((out + 127) / 255) as u8,
}
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?;
if self.a != 255 {
write!(f, "{:02x}", self.a)?;
}
Ok(())
}
}
impl FromStr for Color {
type Err = Error;
fn from_str(text: &str) -> Result<Self> {
if text == "transparent" {
return Ok(Color::TRANSPARENT);
}
let hex = text
.strip_prefix('#')
.filter(|h| (h.len() == 6 || h.len() == 8) && h.bytes().all(|b| b.is_ascii_hexdigit()))
.ok_or_else(|| {
Error::invalid(format!(
"Color `{text}` must be `#rrggbb`, `#rrggbbaa` or `transparent`"
))
})?;
let byte = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).unwrap_or(0);
let a = if hex.len() == 8 { byte(6) } else { 255 };
Ok(Color::rgba(byte(0), byte(2), byte(4), a))
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct Image {
width: u32,
height: u32,
pixels: Vec<u8>,
}
impl fmt::Debug for Image {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Image({}x{})", self.width, self.height)
}
}
impl Image {
pub fn new(width: u32, height: u32) -> Result<Self> {
Self::new_with(width, height, &Limits::default())
}
pub fn new_with(width: u32, height: u32, limits: &Limits) -> Result<Self> {
Self::check_size(width, height, limits)?;
Ok(Self::blank(width, height))
}
pub fn from_pixels(width: u32, height: u32, pixels: Vec<u8>) -> Result<Self> {
if width == 0 || height == 0 {
return Err(Error::invalid("An image needs at least one pixel a side"));
}
if pixels.len() as u64 != width as u64 * height as u64 * 4 {
return Err(Error::invalid(format!(
"An image of {width}x{height} needs {} bytes of RGBA8, not {}",
width as u64 * height as u64 * 4,
pixels.len()
)));
}
Ok(Self {
width,
height,
pixels,
})
}
pub(crate) fn check_size(width: u32, height: u32, limits: &Limits) -> Result<()> {
if width == 0 || height == 0 {
return Err(Error::invalid("An image needs at least one pixel a side"));
}
limits.check_image(width, height)
}
pub(crate) fn blank(width: u32, height: u32) -> Self {
Self {
width,
height,
pixels: vec![0; width as usize * height as usize * 4],
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn pixels(&self) -> &[u8] {
&self.pixels
}
pub fn into_pixels(self) -> Vec<u8> {
self.pixels
}
pub fn pixel(&self, x: u32, y: u32) -> Option<Color> {
if x >= self.width || y >= self.height {
return None;
}
let i = (y as usize * self.width as usize + x as usize) * 4;
Some(Color::from_array([
self.pixels[i],
self.pixels[i + 1],
self.pixels[i + 2],
self.pixels[i + 3],
]))
}
pub fn set_pixel(&mut self, x: u32, y: u32, color: Color) {
if x < self.width && y < self.height {
let i = (y as usize * self.width as usize + x as usize) * 4;
self.pixels[i..i + 4].copy_from_slice(&color.to_array());
}
}
pub(crate) fn blend_pixel(&mut self, x: i64, y: i64, color: Color) {
if x < 0 || y < 0 || x >= self.width as i64 || y >= self.height as i64 {
return;
}
let (x, y) = (x as u32, y as u32);
if let Some(below) = self.pixel(x, y) {
self.set_pixel(x, y, color.over(below));
}
}
pub(crate) fn fill_rect(&mut self, x: i64, y: i64, width: u32, height: u32, color: Color) {
let x0 = x.max(0);
let y0 = y.max(0);
let x1 = (x + width as i64).min(self.width as i64);
let y1 = (y + height as i64).min(self.height as i64);
for py in y0..y1 {
for px in x0..x1 {
self.blend_pixel(px, py, color);
}
}
}
pub fn copy_from(&mut self, source: &Image, x: i64, y: i64) {
for sy in 0..source.height {
let ty = y + sy as i64;
if ty < 0 || ty >= self.height as i64 {
continue;
}
let x0 = x.max(0);
let x1 = (x + source.width as i64).min(self.width as i64);
if x0 >= x1 {
return;
}
let from = (sy as usize * source.width as usize + (x0 - x) as usize) * 4;
let to = (ty as usize * self.width as usize + x0 as usize) * 4;
let len = (x1 - x0) as usize * 4;
self.pixels[to..to + len].copy_from_slice(&source.pixels[from..from + len]);
}
}
pub fn to_png(&self) -> Vec<u8> {
crate::png::encode(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn colors_parse_and_print() {
assert_eq!(
"#1a1c2c".parse::<Color>().unwrap(),
Color::rgb(0x1a, 0x1c, 0x2c)
);
assert_eq!(
"#FF000080".parse::<Color>().unwrap(),
Color::rgba(255, 0, 0, 128)
);
assert_eq!("transparent".parse::<Color>().unwrap(), Color::TRANSPARENT);
for bad in ["", "#", "#fff", "#12345g", "1a1c2c", "#1a1c2c0", "#+1a1c2c"] {
assert!(bad.parse::<Color>().is_err(), "{bad}");
}
assert_eq!(Color::rgb(1, 2, 255).to_string(), "#0102ff");
assert_eq!(Color::rgba(1, 2, 255, 0).to_string(), "#0102ff00");
}
#[test]
fn over_matches_the_operator() {
let red = Color::rgb(255, 0, 0);
let half_blue = Color::rgba(0, 0, 255, 128);
assert_eq!(red.over(Color::WHITE), red);
assert_eq!(Color::TRANSPARENT.over(red), red);
assert_eq!(half_blue.over(Color::TRANSPARENT), half_blue);
assert_eq!(half_blue.over(red), Color::rgba(127, 0, 128, 255));
let a = Color::rgba(200, 100, 0, 100).over(Color::rgba(0, 100, 200, 100));
assert_eq!(a, Color::rgba(124, 100, 76, 161));
}
#[test]
fn copies_clip() {
let mut target = Image::new(4, 4).unwrap();
let mut source = Image::new(3, 3).unwrap();
source.set_pixel(0, 0, Color::WHITE);
source.set_pixel(2, 2, Color::BLACK);
target.copy_from(&source, -2, 2);
assert_eq!(target.pixel(0, 4), None);
assert_eq!(target.pixel(0, 2), Some(Color::TRANSPARENT));
target.copy_from(&source, 2, -2);
assert_eq!(target.pixel(2, 0), Some(Color::TRANSPARENT));
assert_eq!(target.pixel(3, 0), Some(Color::TRANSPARENT));
target.copy_from(&source, 1, 1);
assert_eq!(target.pixel(1, 1), Some(Color::WHITE));
assert_eq!(target.pixel(3, 3), Some(Color::BLACK));
}
}