use crate::{ops::clamp_dimensions, prelude::*, renderer::Rendering};
#[cfg(not(target_arch = "wasm32"))]
use anyhow::Context;
#[cfg(not(target_arch = "wasm32"))]
use png::{BitDepth, ColorType, Decoder};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))]
use std::{
ffi::OsStr,
fs::File,
io::{self, BufReader, BufWriter},
path::{Path, PathBuf},
};
use std::{fmt, iter::Copied, slice};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[must_use]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum PixelFormat {
Rgb,
Rgba,
}
impl PixelFormat {
#[inline]
#[must_use]
pub const fn channels(&self) -> usize {
match self {
PixelFormat::Rgb => 3,
PixelFormat::Rgba => 4,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[doc(hidden)]
pub struct TryFromColorTypeError(pub(crate) ());
#[doc(hidden)]
impl TryFrom<png::ColorType> for PixelFormat {
type Error = TryFromColorTypeError;
fn try_from(color_type: png::ColorType) -> std::result::Result<Self, Self::Error> {
match color_type {
png::ColorType::Rgb => Ok(Self::Rgb),
png::ColorType::Rgba => Ok(Self::Rgba),
_ => Err(TryFromColorTypeError(())),
}
}
}
#[doc(hidden)]
impl From<PixelFormat> for png::ColorType {
fn from(format: PixelFormat) -> Self {
match format {
PixelFormat::Rgb => Self::Rgb,
PixelFormat::Rgba => Self::Rgba,
}
}
}
impl Default for PixelFormat {
fn default() -> Self {
Self::Rgba
}
}
#[derive(Default, Clone)]
#[must_use]
pub struct Image {
width: u32,
height: u32,
data: Vec<u8>,
format: PixelFormat,
}
impl Image {
#[inline]
pub fn new(width: u32, height: u32) -> Self {
Self::rgba(width, height)
}
#[doc(alias = "new")]
#[inline]
pub fn rgba(width: u32, height: u32) -> Self {
let format = PixelFormat::Rgba;
let data = vec![0x00; format.channels() * (width * height) as usize];
Self::from_vec(width, height, data, format)
}
#[inline]
pub fn rgb(width: u32, height: u32) -> Self {
let format = PixelFormat::Rgb;
let data = vec![0x00; format.channels() * (width * height) as usize];
Self::from_vec(width, height, data, format)
}
#[inline]
pub fn from_bytes<B: AsRef<[u8]>>(
width: u32,
height: u32,
bytes: B,
format: PixelFormat,
) -> PixResult<Self> {
let bytes = bytes.as_ref();
if bytes.len() != (format.channels() * width as usize * height as usize) {
return Err(PixError::InvalidImage {
width,
height,
size: bytes.len(),
format,
}
.into());
}
Ok(Self::from_vec(width, height, bytes.to_vec(), format))
}
#[inline]
pub fn from_pixels<P: AsRef<[Color]>>(
width: u32,
height: u32,
pixels: P,
format: PixelFormat,
) -> PixResult<Self> {
let pixels = pixels.as_ref();
if pixels.len() != (width as usize * height as usize) {
return Err(PixError::InvalidImage {
width,
height,
size: pixels.len() * format.channels(),
format,
}
.into());
}
let bytes: Vec<u8> = match format {
PixelFormat::Rgb => pixels
.iter()
.flat_map(|p| [p.red(), p.green(), p.blue()])
.collect(),
PixelFormat::Rgba => pixels.iter().flat_map(Color::channels).collect(),
};
Ok(Self::from_vec(width, height, bytes, format))
}
#[inline]
pub fn from_vec(width: u32, height: u32, data: Vec<u8>, format: PixelFormat) -> Self {
Self {
width,
height,
data,
format,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn from_file<P: AsRef<Path>>(path: P) -> PixResult<Self> {
let path = path.as_ref();
let ext = path.extension();
if ext != Some(OsStr::new("png")) {
return Err(PixError::UnsupportedFileType(ext.map(OsStr::to_os_string)).into());
}
Self::from_read(File::open(path)?)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn from_read<R: io::Read>(read: R) -> PixResult<Self> {
let png_file = BufReader::new(read);
let png = Decoder::new(png_file);
let mut reader = png.read_info().context("failed to read png data")?;
let mut buf = vec![0x00; reader.output_buffer_size()];
let info = reader
.next_frame(&mut buf)
.context("failed to read png data frame")?;
let bit_depth = info.bit_depth;
let color_type = info.color_type;
if bit_depth != BitDepth::Eight || !matches!(color_type, ColorType::Rgb | ColorType::Rgba) {
return Err(PixError::UnsupportedImageFormat {
bit_depth,
color_type,
}
.into());
}
let data = &buf[..info.buffer_size()];
let format = info
.color_type
.try_into()
.map_err(|_| PixError::UnsupportedImageFormat {
bit_depth,
color_type,
})?;
Self::from_bytes(info.width, info.height, data, format)
}
#[inline]
#[must_use]
pub const fn width(&self) -> u32 {
self.width
}
#[inline]
#[must_use]
pub const fn height(&self) -> u32 {
self.height
}
#[inline]
#[must_use]
pub const fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
#[inline]
#[must_use]
pub const fn pitch(&self) -> usize {
self.width() as usize * self.format.channels()
}
#[inline]
pub fn bounding_rect(&self) -> Rect<i32> {
let (width, height) = clamp_dimensions(self.width, self.height);
rect![0, 0, width, height]
}
#[inline]
pub fn bounding_rect_offset<P>(&self, offset: P) -> Rect<i32>
where
P: Into<Point<i32>>,
{
let (width, height) = clamp_dimensions(self.width, self.height);
rect![offset.into(), width, height]
}
#[inline]
pub fn center(&self) -> Point<i32> {
let (width, height) = clamp_dimensions(self.width, self.height);
point!(width / 2, height / 2)
}
#[inline]
pub fn bytes(&self) -> Bytes<'_> {
Bytes(self.as_bytes().iter().copied())
}
#[inline]
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
#[inline]
#[must_use]
pub fn as_mut_bytes(&mut self) -> &mut [u8] {
&mut self.data
}
#[inline]
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn into_bytes(self) -> Vec<u8> {
self.data
}
#[inline]
pub fn pixels(&self) -> Pixels<'_> {
Pixels(self.format.channels(), self.as_bytes().iter().copied())
}
#[inline]
#[must_use]
pub fn into_pixels(self) -> Vec<Color> {
self.data
.chunks(self.format.channels())
.map(|slice| match *slice {
[red, green, blue] => Color::rgb(red, green, blue),
[red, green, blue, alpha] => Color::rgba(red, green, blue, alpha),
_ => Color::TRANSPARENT,
})
.collect()
}
#[inline]
pub fn get_pixel(&self, x: u32, y: u32) -> Color {
let idx = self.idx(x, y);
let channels = self.format.channels();
match self.data.get(idx..idx + channels) {
Some([red, green, blue]) => Color::rgb(*red, *green, *blue),
Some([red, green, blue, alpha]) => Color::rgba(*red, *green, *blue, *alpha),
_ => Color::TRANSPARENT,
}
}
#[inline]
pub fn set_pixel<C: Into<Color>>(&mut self, x: u32, y: u32, color: C) {
let color = color.into();
let idx = self.idx(x, y);
let channels = self.format.channels();
self.data[idx..(idx + channels)].clone_from_slice(&color.channels()[..channels]);
}
#[inline]
pub fn update_bytes<B: AsRef<[u8]>>(&mut self, bytes: B) {
self.data.clone_from_slice(bytes.as_ref());
}
#[inline]
pub const fn format(&self) -> PixelFormat {
self.format
}
#[cfg(not(target_arch = "wasm32"))]
pub fn save<P>(&self, path: P) -> PixResult<()>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let png_file = BufWriter::new(File::create(path)?);
let mut png = png::Encoder::new(png_file, self.width, self.height);
png.set_color(self.format.into());
png.set_depth(png::BitDepth::Eight);
let mut writer = png
.write_header()
.with_context(|| format!("failed to write png header: {path:?}"))?;
writer
.write_image_data(self.as_bytes())
.with_context(|| format!("failed to write png data: {path:?}"))
}
}
impl Image {
#[inline]
const fn idx(&self, x: u32, y: u32) -> usize {
self.format.channels() * (x + y * self.width) as usize
}
}
impl PixState {
pub fn image<P>(&mut self, img: &Image, position: P) -> PixResult<()>
where
P: Into<Point<i32>>,
{
let pos = position.into();
let dst = img.bounding_rect_offset(pos);
self.image_transformed(img, None, dst, 0.0, None, None)
}
pub fn image_transformed<R1, R2, A, C, F>(
&mut self,
img: &Image,
src: R1,
dst: R2,
angle: A,
center: C,
flipped: F,
) -> PixResult<()>
where
R1: Into<Option<Rect<i32>>>,
R2: Into<Option<Rect<i32>>>,
A: Into<Option<f64>>,
C: Into<Option<Point<i32>>>,
F: Into<Option<Flipped>>,
{
let s = &self.settings;
let mut dst = dst.into();
if s.image_mode == ImageMode::Center {
dst = dst.map(|dst| Rect::from_center(dst.top_left(), dst.width(), dst.height()));
};
let mut angle = angle.into().unwrap_or(0.0);
if s.angle_mode == AngleMode::Radians {
angle = angle.to_degrees();
};
self.renderer.image(
img,
src.into(),
dst,
angle,
center.into(),
flipped.into(),
s.image_tint,
)
}
}
impl fmt::Debug for Image {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Image")
.field("width", &self.width)
.field("height", &self.height)
.field("size", &self.data.len())
.field("format", &self.format)
.finish()
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct Bytes<'a>(Copied<slice::Iter<'a, u8>>);
impl Iterator for Bytes<'_> {
type Item = u8;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct Pixels<'a>(usize, Copied<slice::Iter<'a, u8>>);
impl Iterator for Pixels<'_> {
type Item = Color;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let r = self.1.next()?;
let g = self.1.next()?;
let b = self.1.next()?;
let channels = self.0;
match channels {
3 => Some(Color::rgb(r, g, b)),
4 => {
let a = self.1.next()?;
Some(Color::rgba(r, g, b, a))
}
_ => Some(Color::TRANSPARENT),
}
}
}
#[derive(Debug, Clone)]
pub enum Icon {
Image(Image),
#[cfg(not(target_arch = "wasm32"))]
Path(PathBuf),
}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Into<PathBuf>> From<T> for Icon {
fn from(value: T) -> Self {
Self::Path(value.into())
}
}
impl From<Image> for Icon {
fn from(img: Image) -> Self {
Self::Image(img)
}
}