fastpack_core/imaging/
loader.rs1use std::hash::Hasher;
2use std::path::{Path, PathBuf};
3
4use image::DynamicImage;
5use rayon::prelude::*;
6use rustc_hash::FxHasher;
7
8use crate::{
9 error::CoreError,
10 types::{rect::Size, sprite::Sprite},
11};
12
13pub fn load(path: &Path, id: impl Into<String>) -> Result<Sprite, CoreError> {
22 let id = id.into();
23 let image = decode(path)?;
24 let rgba = image.into_rgba8();
25 let (w, h) = rgba.dimensions();
26 let content_hash = hash_rgba(&rgba);
27 Ok(Sprite {
28 id,
29 source_path: path.to_path_buf(),
30 image: DynamicImage::ImageRgba8(rgba),
31 trim_rect: None,
32 original_size: Size { w, h },
33 polygon: None,
34 nine_patch: None,
35 pivot: None,
36 content_hash,
37 extrude: 0,
38 alias_of: None,
39 })
40}
41
42pub fn load_many(paths: &[(PathBuf, String)]) -> Vec<Result<Sprite, CoreError>> {
47 paths
48 .par_iter()
49 .map(|(path, id)| load(path, id.clone()))
50 .collect()
51}
52
53fn decode(path: &Path) -> Result<DynamicImage, CoreError> {
54 let ext = path
55 .extension()
56 .and_then(|s| s.to_str())
57 .unwrap_or("")
58 .to_lowercase();
59
60 match ext.as_str() {
61 #[cfg(feature = "svg")]
62 "svg" => decode_svg(path),
63 #[cfg(feature = "psd")]
64 "psd" => decode_psd(path),
65 _ => image::open(path).map_err(|source| CoreError::ImageLoad {
66 path: path.to_path_buf(),
67 source,
68 }),
69 }
70}
71
72fn hash_rgba(img: &image::RgbaImage) -> u64 {
73 let mut h = FxHasher::default();
74 h.write(img.as_raw());
75 h.finish()
76}
77
78#[cfg(feature = "svg")]
79fn decode_svg(path: &Path) -> Result<DynamicImage, CoreError> {
80 let data = std::fs::read(path).map_err(CoreError::Io)?;
81 let opts = resvg::usvg::Options::default();
82 let tree = resvg::usvg::Tree::from_data(&data, &opts).map_err(|e| {
83 CoreError::Io(std::io::Error::new(
84 std::io::ErrorKind::InvalidData,
85 e.to_string(),
86 ))
87 })?;
88 let w = tree.size().width().ceil() as u32;
89 let h = tree.size().height().ceil() as u32;
90 if w == 0 || h == 0 {
91 return Err(CoreError::Io(std::io::Error::new(
92 std::io::ErrorKind::InvalidData,
93 "SVG has zero-size viewport",
94 )));
95 }
96 let mut pixmap = resvg::tiny_skia::Pixmap::new(w, h).ok_or_else(|| {
97 CoreError::Io(std::io::Error::new(
98 std::io::ErrorKind::InvalidData,
99 "could not allocate SVG pixmap",
100 ))
101 })?;
102 resvg::render(
103 &tree,
104 resvg::tiny_skia::Transform::default(),
105 &mut pixmap.as_mut(),
106 );
107 let mut raw = pixmap.take();
109 demultiply_alpha(&mut raw);
110 image::RgbaImage::from_raw(w, h, raw)
111 .map(DynamicImage::ImageRgba8)
112 .ok_or_else(|| {
113 CoreError::Io(std::io::Error::new(
114 std::io::ErrorKind::InvalidData,
115 "SVG pixmap buffer size mismatch",
116 ))
117 })
118}
119
120#[cfg(feature = "svg")]
121fn demultiply_alpha(data: &mut [u8]) {
122 for pixel in data.chunks_exact_mut(4) {
123 let a = pixel[3];
124 if a == 0 {
125 pixel[0] = 0;
126 pixel[1] = 0;
127 pixel[2] = 0;
128 } else if a < 255 {
129 let inv = 255.0 / a as f32;
130 pixel[0] = (pixel[0] as f32 * inv).round().min(255.0) as u8;
131 pixel[1] = (pixel[1] as f32 * inv).round().min(255.0) as u8;
132 pixel[2] = (pixel[2] as f32 * inv).round().min(255.0) as u8;
133 }
134 }
135}
136
137#[cfg(feature = "psd")]
138fn decode_psd(path: &Path) -> Result<DynamicImage, CoreError> {
139 let data = std::fs::read(path).map_err(CoreError::Io)?;
140 let psd_file = psd::Psd::from_bytes(&data).map_err(|e| {
141 CoreError::Io(std::io::Error::new(
142 std::io::ErrorKind::InvalidData,
143 e.to_string(),
144 ))
145 })?;
146 let w = psd_file.width();
147 let h = psd_file.height();
148 image::RgbaImage::from_raw(w, h, psd_file.rgba())
149 .map(DynamicImage::ImageRgba8)
150 .ok_or_else(|| {
151 CoreError::Io(std::io::Error::new(
152 std::io::ErrorKind::InvalidData,
153 "PSD buffer size mismatch",
154 ))
155 })
156}