use std::fmt;
use std::fs::File;
use std::io::{self, BufReader, Read, Seek, SeekFrom};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::color::Rgb;
use crate::i18n::translate_active;
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Kind {
Png,
Jpeg,
Gif,
WebP,
}
impl Kind {
pub(super) fn from_format(format: ::image::ImageFormat) -> Option<Self> {
match format {
::image::ImageFormat::Png => Some(Self::Png),
::image::ImageFormat::Jpeg => Some(Self::Jpeg),
::image::ImageFormat::Gif => Some(Self::Gif),
::image::ImageFormat::WebP => Some(Self::WebP),
_ => None,
}
}
pub(super) fn label(self) -> &'static str {
match self {
Self::Png => "PNG",
Self::Jpeg => "JPEG",
Self::Gif => "GIF",
Self::WebP => "WebP",
}
}
}
struct Inner {
id: u64,
width: u32,
height: u32,
pixels: Box<[Rgb]>,
original: (u32, u32),
name: Option<String>,
kind: Option<Kind>,
}
#[derive(Clone)]
pub struct ImageData {
inner: Arc<Inner>,
}
impl fmt::Debug for ImageData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ImageData")
.field("width", &self.inner.width)
.field("height", &self.inner.height)
.field("original", &self.inner.original)
.field("name", &self.inner.name)
.finish_non_exhaustive()
}
}
impl ImageData {
fn new(width: u32, height: u32, pixels: Box<[Rgb]>, original: (u32, u32)) -> Self {
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
Self { inner: Arc::new(Inner { id, width, height, pixels, original, name: None, kind: None }) }
}
#[must_use]
pub fn from_rgb(width: u32, height: u32, rgb: &[u8]) -> Option<Self> {
let count = usize::try_from(u64::from(width) * u64::from(height)).ok()?;
if count == 0 || rgb.len() != count.checked_mul(3)? {
return None;
}
let pixels = rgb.chunks_exact(3).map(|c| Rgb::new(c[0], c[1], c[2])).collect();
Some(Self::new(width, height, pixels, (width, height)))
}
pub fn decode_file(path: &Path, max: (u32, u32)) -> Result<Self, ImageError> {
let mut file = File::open(path).map_err(|error| ImageError::from_io(&error))?;
let mut header = [0u8; 32];
let mut read = 0;
while read < header.len() {
match file.read(&mut header[read..]) {
Ok(0) => break,
Ok(count) => read += count,
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(error) => return Err(ImageError::from_io(&error)),
}
}
let (format, kind) = Self::format_of(&header[..read])?;
file.seek(SeekFrom::Start(0)).map_err(|error| ImageError::from_io(&error))?;
let name = path.file_name().map(|name| name.to_string_lossy().into_owned());
Self::decode(BufReader::new(file), format, kind, max, name)
}
pub fn decode_bytes(bytes: &[u8], max: (u32, u32)) -> Result<Self, ImageError> {
let (format, kind) = Self::format_of(bytes)?;
Self::decode(io::Cursor::new(bytes), format, kind, max, None)
}
fn format_of(header: &[u8]) -> Result<(::image::ImageFormat, Kind), ImageError> {
let format = ::image::guess_format(header).map_err(|_| ImageError::UnknownFormat)?;
let kind = Kind::from_format(format).ok_or(ImageError::UnknownFormat)?;
Ok((format, kind))
}
fn decode(
reader: impl io::BufRead + Seek,
format: ::image::ImageFormat,
kind: Kind,
max: (u32, u32),
name: Option<String>,
) -> Result<Self, ImageError> {
let reader = ::image::ImageReader::with_format(reader, format);
let mut picture = reader.decode().map_err(|error| ImageError::from_decoding(&error))?;
let original = (picture.width(), picture.height());
if original.0 == 0 || original.1 == 0 {
return Err(ImageError::Broken);
}
let (max_width, max_height) = (max.0.max(1), max.1.max(1));
if original.0 > max_width || original.1 > max_height {
picture = picture.thumbnail(max_width, max_height);
}
let rgb = picture.into_rgb8();
let (width, height) = rgb.dimensions();
let pixels = rgb.pixels().map(|p| Rgb::new(p.0[0], p.0[1], p.0[2])).collect();
let mut data = Self::new(width, height, pixels, original);
if let Some(inner) = Arc::get_mut(&mut data.inner) {
inner.name = name;
inner.kind = Some(kind);
}
Ok(data)
}
pub const EXTENSIONS: &'static [&'static str] = &["png", "jpg", "jpeg", "gif", "webp"];
#[must_use]
pub fn reads(path: &Path) -> bool {
path.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| Self::EXTENSIONS.iter().any(|known| known.eq_ignore_ascii_case(extension)))
}
#[must_use]
pub fn width(&self) -> u32 {
self.inner.width
}
#[must_use]
pub fn height(&self) -> u32 {
self.inner.height
}
#[must_use]
pub fn original_size(&self) -> (u32, u32) {
self.inner.original
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.inner.name.as_deref()
}
#[must_use]
pub fn pixel(&self, x: u32, y: u32) -> Option<Rgb> {
if x >= self.inner.width || y >= self.inner.height {
return None;
}
let index = usize::try_from(u64::from(y) * u64::from(self.inner.width) + u64::from(x)).ok()?;
self.inner.pixels.get(index).copied()
}
pub(crate) fn pixels(&self) -> &[Rgb] {
&self.inner.pixels
}
pub(crate) fn id(&self) -> u64 {
self.inner.id
}
pub(super) fn kind(&self) -> Option<Kind> {
self.inner.kind
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageError {
Missing,
Unreadable,
UnknownFormat,
Broken,
}
impl ImageError {
fn from_io(error: &io::Error) -> Self {
match error.kind() {
io::ErrorKind::NotFound => Self::Missing,
_ => Self::Unreadable,
}
}
fn from_decoding(error: &::image::ImageError) -> Self {
use ::image::ImageError as E;
match error {
E::Unsupported(_) => Self::UnknownFormat,
E::Limits(_) => Self::Unreadable,
E::Decoding(_) | E::Parameter(_) | E::Encoding(_) | E::IoError(_) => Self::Broken,
}
}
fn key(self) -> &'static str {
match self {
Self::Missing => "quvyta.image.missing",
Self::Unreadable => "quvyta.image.unreadable",
Self::UnknownFormat => "quvyta.image.unknown-format",
Self::Broken => "quvyta.image.broken",
}
}
}
impl fmt::Display for ImageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&translate_active(self.key(), &[]))
}
}
impl std::error::Error for ImageError {}