use std::cell::RefCell;
use std::fmt;
use std::path::Path;
use std::rc::Rc;
use tiny_skia::{ColorU8, IntSize, Pixmap};
use crate::geometry::{Color, Size};
pub const PLACEHOLDER_SIZE: i32 = 48;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VisualState {
#[default]
Normal,
Hover,
Pressed,
Selected,
Disabled,
}
impl VisualState {
pub fn opacity(self) -> f32 {
match self {
VisualState::Disabled => 0.38,
_ => 1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Fit {
#[default]
Contain,
Cover,
Fill,
None,
}
#[derive(Debug)]
pub enum ImageError {
Io(std::io::Error),
UnsupportedFormat,
Decode(String),
InvalidRgba,
}
impl fmt::Display for ImageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImageError::Io(e) => write!(f, "图片读取失败: {e}"),
ImageError::UnsupportedFormat => write!(f, "无可用解码器识别该图片格式"),
ImageError::Decode(m) => write!(f, "图片解码失败: {m}"),
ImageError::InvalidRgba => write!(f, "RGBA 数据长度与 width*height*4 不符"),
}
}
}
impl std::error::Error for ImageError {}
impl From<std::io::Error> for ImageError {
fn from(e: std::io::Error) -> Self {
ImageError::Io(e)
}
}
pub struct DecodedImage {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
pub trait ImageDecoder {
fn probe(&self, bytes: &[u8]) -> bool;
fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, ImageError>;
fn name(&self) -> &'static str;
}
struct PngDecoder;
const PNG_MAGIC: &[u8] = &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
impl ImageDecoder for PngDecoder {
fn probe(&self, bytes: &[u8]) -> bool {
bytes.len() >= PNG_MAGIC.len() && &bytes[..PNG_MAGIC.len()] == PNG_MAGIC
}
fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, ImageError> {
let pm = Pixmap::decode_png(bytes).map_err(|e| ImageError::Decode(format!("{e}")))?;
let (w, h) = (pm.width(), pm.height());
let mut rgba = Vec::with_capacity((w as usize) * (h as usize) * 4);
for p in pm.pixels() {
let c = p.demultiply();
rgba.extend_from_slice(&[c.red(), c.green(), c.blue(), c.alpha()]);
}
Ok(DecodedImage {
width: w,
height: h,
rgba,
})
}
fn name(&self) -> &'static str {
"png"
}
}
#[cfg(feature = "svg")]
struct SvgDecoder;
#[cfg(feature = "svg")]
impl SvgDecoder {
fn rasterize(bytes: &[u8], target_width: Option<u32>) -> Result<DecodedImage, ImageError> {
use resvg::{tiny_skia, usvg};
#[allow(unused_mut)]
let mut opt = usvg::Options::default();
#[cfg(feature = "svg-text")]
{
thread_local! {
static FONTDB: std::sync::Arc<usvg::fontdb::Database> = {
let mut db = usvg::fontdb::Database::new();
db.load_system_fonts();
std::sync::Arc::new(db)
};
}
opt.fontdb = FONTDB.with(|db| db.clone());
}
let tree = usvg::Tree::from_data(bytes, &opt)
.map_err(|e| ImageError::Decode(format!("SVG 解析失败: {e}")))?;
let size = tree.size();
let (iw, ih) = (size.width(), size.height());
if iw <= 0.0 || ih <= 0.0 {
return Err(ImageError::Decode("SVG 固有尺寸非法".into()));
}
let (tw, th) = match target_width {
Some(w) if w > 0 => {
let scale = w as f32 / iw;
(w, (ih * scale).round().max(1.0) as u32)
}
_ => (iw.ceil() as u32, ih.ceil() as u32),
};
let mut pm = tiny_skia::Pixmap::new(tw, th)
.ok_or_else(|| ImageError::Decode("SVG 目标尺寸过大或非法".into()))?;
let transform = tiny_skia::Transform::from_scale(tw as f32 / iw, th as f32 / ih);
resvg::render(&tree, transform, &mut pm.as_mut());
let (w, h) = (pm.width(), pm.height());
let mut rgba = Vec::with_capacity((w as usize) * (h as usize) * 4);
for p in pm.pixels() {
let c = p.demultiply();
rgba.extend_from_slice(&[c.red(), c.green(), c.blue(), c.alpha()]);
}
Ok(DecodedImage {
width: w,
height: h,
rgba,
})
}
}
#[cfg(feature = "svg")]
impl ImageDecoder for SvgDecoder {
fn probe(&self, bytes: &[u8]) -> bool {
if bytes.starts_with(&[0x1f, 0x8b]) {
return true;
}
let n = bytes.len().min(1024);
bytes[..n]
.windows(4)
.any(|w| w.eq_ignore_ascii_case(b"<svg"))
}
fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, ImageError> {
Self::rasterize(bytes, None)
}
fn name(&self) -> &'static str {
"svg"
}
}
thread_local! {
static DECODERS: RefCell<Vec<Box<dyn ImageDecoder>>> = RefCell::new({
#[allow(unused_mut)]
let mut v: Vec<Box<dyn ImageDecoder>> = vec![Box::new(PngDecoder)];
#[cfg(feature = "svg")]
v.push(Box::new(SvgDecoder));
v
});
}
pub fn register_decoder(decoder: Box<dyn ImageDecoder>) {
DECODERS.with(|r| r.borrow_mut().push(decoder));
}
fn decode_with_registry(bytes: &[u8]) -> Result<DecodedImage, ImageError> {
DECODERS.with(|r| {
let decoders = r.borrow();
for d in decoders.iter().rev() {
if d.probe(bytes) {
return d.decode(bytes);
}
}
Err(ImageError::UnsupportedFormat)
})
}
#[derive(Clone)]
pub struct Image {
pixmap: Rc<Pixmap>,
w: u32,
h: u32,
}
impl Image {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ImageError> {
let bytes = std::fs::read(path)?;
Self::from_bytes(&bytes)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ImageError> {
let decoded = decode_with_registry(bytes)?;
Self::from_decoded(decoded)
}
pub fn from_png_bytes(bytes: &[u8]) -> Result<Self, ImageError> {
let pm = Pixmap::decode_png(bytes).map_err(|e| ImageError::Decode(format!("{e}")))?;
Ok(Self::from_pixmap(pm))
}
#[cfg(feature = "svg")]
pub fn from_svg_bytes(bytes: &[u8], target_width: Option<u32>) -> Result<Self, ImageError> {
Self::from_decoded(SvgDecoder::rasterize(bytes, target_width)?)
}
pub fn from_rgba(width: u32, height: u32, rgba: &[u8]) -> Result<Self, ImageError> {
if width == 0 || height == 0 {
return Err(ImageError::InvalidRgba);
}
let expect = (width as usize) * (height as usize) * 4;
if rgba.len() != expect {
return Err(ImageError::InvalidRgba);
}
Self::from_decoded(DecodedImage {
width,
height,
rgba: rgba.to_vec(),
})
}
fn from_decoded(d: DecodedImage) -> Result<Self, ImageError> {
let size = IntSize::from_wh(d.width, d.height).ok_or(ImageError::InvalidRgba)?;
let mut data = Vec::with_capacity(d.rgba.len());
for px in d.rgba.chunks_exact(4) {
let c = ColorU8::from_rgba(px[0], px[1], px[2], px[3]).premultiply();
data.extend_from_slice(&[c.red(), c.green(), c.blue(), c.alpha()]);
}
let pm = Pixmap::from_vec(data, size).ok_or(ImageError::InvalidRgba)?;
Ok(Self::from_pixmap(pm))
}
fn from_pixmap(pm: Pixmap) -> Self {
let (w, h) = (pm.width(), pm.height());
Self {
pixmap: Rc::new(pm),
w,
h,
}
}
pub fn size(&self) -> Size {
Size::new(self.w as i32, self.h as i32)
}
pub fn tinted(&self, color: Color) -> Image {
let size = IntSize::from_wh(self.w, self.h).expect("尺寸已在构造时校验");
let mut data = Vec::with_capacity((self.w as usize) * (self.h as usize) * 4);
for p in self.pixmap.pixels() {
let a = ((p.alpha() as u16 * color.a as u16) / 255) as u8;
let c = ColorU8::from_rgba(color.r, color.g, color.b, a).premultiply();
data.extend_from_slice(&[c.red(), c.green(), c.blue(), c.alpha()]);
}
let pm = Pixmap::from_vec(data, size).expect("着色 Pixmap 尺寸匹配");
Self {
pixmap: Rc::new(pm),
w: self.w,
h: self.h,
}
}
pub fn width(&self) -> u32 {
self.w
}
pub fn height(&self) -> u32 {
self.h
}
pub(crate) fn pixmap(&self) -> &Pixmap {
&self.pixmap
}
#[cfg_attr(not(all(windows, feature = "d2d")), allow(dead_code))]
pub(crate) fn cache_id(&self) -> usize {
std::rc::Rc::as_ptr(&self.pixmap) as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
fn red_png(w: u32, h: u32) -> Vec<u8> {
let mut pm = Pixmap::new(w, h).unwrap();
pm.fill(tiny_skia::Color::from_rgba8(255, 0, 0, 255));
pm.encode_png().unwrap()
}
#[test]
fn from_png_bytes_reports_size() {
let png = red_png(4, 3);
let img = Image::from_png_bytes(&png).unwrap();
assert_eq!(img.size(), Size::new(4, 3));
}
#[test]
fn from_bytes_sniffs_png() {
let png = red_png(2, 2);
let img = Image::from_bytes(&png).unwrap();
assert_eq!(img.size(), Size::new(2, 2));
}
#[test]
fn from_bytes_rejects_unknown_format() {
let junk = [0u8, 1, 2, 3, 4, 5, 6, 7];
assert!(matches!(
Image::from_bytes(&junk),
Err(ImageError::UnsupportedFormat)
));
}
#[test]
fn from_rgba_validates_length() {
let ok = Image::from_rgba(2, 2, &[0u8; 16]);
assert!(ok.is_ok());
assert!(matches!(
Image::from_rgba(2, 2, &[0u8; 15]),
Err(ImageError::InvalidRgba)
));
assert!(matches!(
Image::from_rgba(0, 2, &[]),
Err(ImageError::InvalidRgba)
));
}
#[test]
fn from_rgba_preserves_size() {
let img = Image::from_rgba(5, 7, &[128u8; 5 * 7 * 4]).unwrap();
assert_eq!(img.size(), Size::new(5, 7));
}
#[test]
fn tinted_recolors_keeping_alpha() {
let src = Image::from_rgba(2, 1, &[255, 255, 255, 255, 255, 255, 255, 128]).unwrap();
let red = src.tinted(Color::rgb(255, 0, 0));
assert_eq!(red.size(), Size::new(2, 1));
let pm = red.pixmap();
let p0 = pm.pixel(0, 0).unwrap().demultiply();
assert_eq!(
(p0.red(), p0.green(), p0.blue(), p0.alpha()),
(255, 0, 0, 255)
);
let p1 = pm.pixel(1, 0).unwrap().demultiply();
assert_eq!((p1.red(), p1.green(), p1.blue()), (255, 0, 0));
assert!(p1.alpha() < 200, "半透明 alpha 应保留,实得 {}", p1.alpha());
}
#[test]
fn visual_state_opacity() {
assert_eq!(VisualState::Disabled.opacity(), 0.38);
assert_eq!(VisualState::Normal.opacity(), 1.0);
assert_eq!(VisualState::Hover.opacity(), 1.0);
}
#[test]
fn png_decoder_probe() {
assert!(PngDecoder.probe(PNG_MAGIC));
assert!(!PngDecoder.probe(&[0, 1, 2]));
assert!(!PngDecoder.probe(&[0xff, 0xd8])); }
#[cfg(feature = "svg")]
mod svg {
use super::*;
const RED_SVG: &[u8] =
br##"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="6"><rect width="10" height="6" fill="#ff0000"/></svg>"##;
#[test]
fn svg_decoder_probe() {
assert!(SvgDecoder.probe(RED_SVG));
assert!(SvgDecoder.probe(br#"<?xml version="1.0"?><svg></svg>"#));
assert!(SvgDecoder.probe(b"<SVG></SVG>"));
assert!(SvgDecoder.probe(&[0x1f, 0x8b, 0x08, 0x00]));
assert!(!SvgDecoder.probe(PNG_MAGIC));
assert!(!SvgDecoder.probe(&[0, 1, 2, 3]));
}
#[test]
fn from_svg_bytes_uses_intrinsic_size() {
let img = Image::from_svg_bytes(RED_SVG, None).unwrap();
assert_eq!(img.size(), Size::new(10, 6));
}
#[test]
fn from_svg_bytes_target_width_scales_keeping_aspect() {
let img = Image::from_svg_bytes(RED_SVG, Some(20)).unwrap();
assert_eq!(img.size(), Size::new(20, 12));
}
#[test]
fn from_bytes_sniffs_svg_via_registry() {
let img = Image::from_bytes(RED_SVG).unwrap();
assert_eq!(img.size(), Size::new(10, 6));
}
#[test]
fn rasterized_svg_has_red_pixel() {
let img = Image::from_svg_bytes(RED_SVG, Some(20)).unwrap();
let p = img.pixmap().pixel(10, 6).unwrap().demultiply();
assert!(
p.red() > 200 && p.green() < 60 && p.blue() < 60,
"中心应为红色,实得 ({},{},{})",
p.red(),
p.green(),
p.blue()
);
}
#[test]
fn invalid_svg_is_decode_error() {
assert!(matches!(
Image::from_svg_bytes(b"<svg not valid", None),
Err(ImageError::Decode(_))
));
}
#[cfg(feature = "svg-text")]
#[test]
fn renders_svg_text_glyphs() {
const TEXT_SVG: &[u8] =
br##"<svg xmlns="http://www.w3.org/2000/svg" width="80" height="40"><text x="4" y="30" font-size="30" font-family="Arial" fill="#000000">Hi</text></svg>"##;
let img = Image::from_svg_bytes(TEXT_SVG, None).unwrap();
let opaque = img
.pixmap()
.pixels()
.iter()
.filter(|p| p.alpha() > 0)
.count();
assert!(opaque > 0, "文字 SVG 应光栅出字形像素,实得 {opaque}");
}
}
}