mimick/library/texture_decode/
codecs.rs1pub(super) fn decode_jpegxl_texture(path: &std::path::Path) -> Option<gdk4::Texture> {
2 let render = load_jxl_render(path)?;
3 let (width, height, format, bpp, buf) = read_jxl_pixels(&render, path)?;
4 super::memory_texture(
5 width,
6 height,
7 format,
8 buf,
9 usize::try_from(width).ok()?.checked_mul(bpp)?,
10 )
11}
12
13fn read_jxl_pixels(
14 render: &jxl_oxide::Render,
15 path: &std::path::Path,
16) -> Option<(u32, u32, gdk4::MemoryFormat, usize, Vec<u8>)> {
17 let mut stream = render.stream();
18 let width = stream.width();
19 let height = stream.height();
20 let channels = stream.channels();
21 let pixel_count = usize::try_from(width)
22 .ok()?
23 .checked_mul(usize::try_from(height).ok()?)?;
24 let mut buf = vec![0u8; pixel_count.checked_mul(channels as usize)?];
25 stream.write_to_buffer::<u8>(&mut buf);
26 let (format, bpp, buf) = normalize_jxl_pixels(buf, channels, pixel_count, path)?;
27 Some((width, height, format, bpp, buf))
28}
29
30fn load_jxl_render(path: &std::path::Path) -> Option<jxl_oxide::Render> {
31 let file = match std::fs::File::open(path) {
32 Ok(f) => f,
33 Err(err) => {
34 log::warn!("JXL read failed for {}: {}", path.display(), err);
35 return None;
36 }
37 };
38 let image = match jxl_oxide::JxlImage::builder().read(file) {
39 Ok(img) => img,
40 Err(err) => {
41 log::warn!("JXL parse failed for {}: {}", path.display(), err);
42 return None;
43 }
44 };
45 match image.render_frame(0) {
46 Ok(render) => Some(render),
47 Err(err) => {
48 log::warn!("JXL render failed for {}: {}", path.display(), err);
49 None
50 }
51 }
52}
53
54pub(super) fn decode_svg_texture(path: &std::path::Path) -> Option<gdk4::Texture> {
55 let bytes = match std::fs::read(path) {
56 Ok(bytes) => bytes,
57 Err(err) => {
58 log::warn!("SVG read failed for {}: {}", path.display(), err);
59 return None;
60 }
61 };
62 let opt = resvg::usvg::Options::default();
63 let prepared = inject_missing_xmlns(&bytes);
64 let tree = match resvg::usvg::Tree::from_data(&prepared, &opt) {
65 Ok(tree) => tree,
66 Err(err) => {
67 log::warn!("SVG parse failed for {}: {}", path.display(), err);
68 return None;
69 }
70 };
71 let (width, height, scale) = scaled_svg_size(tree.size());
72 let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height)?;
73 let transform = resvg::tiny_skia::Transform::from_scale(scale, scale);
74 resvg::render(&tree, transform, &mut pixmap.as_mut());
75 super::memory_texture(
76 width,
77 height,
78 gdk4::MemoryFormat::R8g8b8a8,
79 pixmap.take(),
80 usize::try_from(width).ok()?.checked_mul(4)?,
81 )
82}
83
84pub(super) fn decode_jpeg2k_texture(path: &std::path::Path) -> Option<gdk4::Texture> {
85 let bytes = match std::fs::read(path) {
86 Ok(bytes) => bytes,
87 Err(err) => {
88 log::warn!("JP2 read failed for {}: {}", path.display(), err);
89 return None;
90 }
91 };
92 let image = match jpeg2k::Image::from_bytes(&bytes) {
93 Ok(image) => image,
94 Err(err) => {
95 log::warn!("JP2 parse failed for {}: {}", path.display(), err);
96 return None;
97 }
98 };
99 let pixels = match image.get_pixels(Some(255)) {
100 Ok(pixels) => pixels,
101 Err(err) => {
102 log::warn!("JP2 pixel extract failed for {}: {}", path.display(), err);
103 return None;
104 }
105 };
106 let width = pixels.width;
107 let height = pixels.height;
108 let (format, bytes, bpp) = normalize_jpeg2k_pixels(pixels.data, width, height, path)?;
109 super::memory_texture(
110 width,
111 height,
112 format,
113 bytes,
114 usize::try_from(width).ok()?.checked_mul(bpp)?,
115 )
116}
117
118fn normalize_jxl_pixels(
119 buf: Vec<u8>,
120 channels: u32,
121 pixel_count: usize,
122 path: &std::path::Path,
123) -> Option<(gdk4::MemoryFormat, usize, Vec<u8>)> {
124 match channels {
125 1 => Some((
126 gdk4::MemoryFormat::R8g8b8,
127 3,
128 expand_gray_to_rgb(buf, pixel_count)?,
129 )),
130 2 => Some((
131 gdk4::MemoryFormat::R8g8b8a8,
132 4,
133 expand_gray_alpha_to_rgba(buf, pixel_count)?,
134 )),
135 3 => Some((gdk4::MemoryFormat::R8g8b8, 3, buf)),
136 4 => Some((gdk4::MemoryFormat::R8g8b8a8, 4, buf)),
137 _ => {
138 log::warn!(
139 "JXL unsupported channel count {} for {}",
140 channels,
141 path.display()
142 );
143 None
144 }
145 }
146}
147
148fn normalize_jpeg2k_pixels(
149 data: jpeg2k::ImagePixelData,
150 width: u32,
151 height: u32,
152 path: &std::path::Path,
153) -> Option<(gdk4::MemoryFormat, Vec<u8>, usize)> {
154 let pixel_count = usize::try_from(width)
155 .ok()?
156 .checked_mul(usize::try_from(height).ok()?)?;
157 match data {
158 jpeg2k::ImagePixelData::Rgb8(data) => Some((gdk4::MemoryFormat::R8g8b8, data, 3)),
159 jpeg2k::ImagePixelData::Rgba8(data) => Some((gdk4::MemoryFormat::R8g8b8a8, data, 4)),
160 jpeg2k::ImagePixelData::L8(data) => Some((
161 gdk4::MemoryFormat::R8g8b8,
162 expand_gray_to_rgb(data, pixel_count)?,
163 3,
164 )),
165 jpeg2k::ImagePixelData::La8(data) => Some((
166 gdk4::MemoryFormat::R8g8b8a8,
167 expand_gray_alpha_to_rgba(data, pixel_count)?,
168 4,
169 )),
170 _ => {
171 log::warn!("JP2 unsupported 16-bit pixel layout for {}", path.display());
172 None
173 }
174 }
175}
176
177fn expand_gray_to_rgb(data: Vec<u8>, pixel_count: usize) -> Option<Vec<u8>> {
178 let mut rgb = Vec::with_capacity(pixel_count.checked_mul(3)?);
179 for v in data {
180 rgb.extend_from_slice(&[v, v, v]);
181 }
182 Some(rgb)
183}
184
185fn expand_gray_alpha_to_rgba(data: Vec<u8>, pixel_count: usize) -> Option<Vec<u8>> {
186 let mut rgba = Vec::with_capacity(pixel_count.checked_mul(4)?);
187 for chunk in data.as_chunks::<2>().0 {
188 rgba.extend_from_slice(&[chunk[0], chunk[0], chunk[0], chunk[1]]);
189 }
190 Some(rgba)
191}
192
193const SVG_MAX_DIMENSION: u32 = 4096;
194
195fn scaled_svg_size(svg_size: resvg::usvg::Size) -> (u32, u32, f32) {
196 let svg_w = svg_size.width().max(1.0);
197 let svg_h = svg_size.height().max(1.0);
198 let scale = (SVG_MAX_DIMENSION as f32 / svg_w)
199 .min(SVG_MAX_DIMENSION as f32 / svg_h)
200 .min(1.0);
201 (
202 (svg_w * scale).ceil() as u32,
203 (svg_h * scale).ceil() as u32,
204 scale,
205 )
206}
207
208fn inject_missing_xmlns(bytes: &[u8]) -> Vec<u8> {
209 let Ok(text) = std::str::from_utf8(bytes) else {
210 return bytes.to_vec();
211 };
212 let declared = scan_declared_prefixes(text);
213 let used = scan_used_prefixes(text, &declared);
214 if used.is_empty() {
215 return bytes.to_vec();
216 }
217 let Some(svg_tag) = text.find("<svg") else {
218 return bytes.to_vec();
219 };
220 insert_xmlns_decls(bytes, svg_tag + 4, &used)
221}
222
223fn insert_xmlns_decls(
224 bytes: &[u8],
225 insert_at: usize,
226 used: &std::collections::HashSet<String>,
227) -> Vec<u8> {
228 let mut decls = String::new();
229 for prefix in used {
230 decls.push_str(&format!(
231 " xmlns:{prefix}=\"urn:mimick:placeholder:{prefix}\""
232 ));
233 }
234 let mut out = Vec::with_capacity(bytes.len() + decls.len());
235 out.extend_from_slice(&bytes[..insert_at]);
236 out.extend_from_slice(decls.as_bytes());
237 out.extend_from_slice(&bytes[insert_at..]);
238 out
239}
240
241fn scan_declared_prefixes(text: &str) -> std::collections::HashSet<String> {
242 let mut declared: std::collections::HashSet<String> = ["xml", "xmlns", "xlink"]
243 .into_iter()
244 .map(String::from)
245 .collect();
246 let bytes_str = text.as_bytes();
247 let mut i = 0;
248 while let Some(rel) = text[i..].find("xmlns:") {
249 let start = i + rel + 6;
250 let end = xmlns_prefix_end(bytes_str, start);
251 if end > start {
252 declared.insert(text[start..end].to_string());
253 }
254 i = end;
255 }
256 declared
257}
258
259fn xmlns_prefix_end(bytes_str: &[u8], mut end: usize) -> usize {
260 while end < bytes_str.len() && !is_xmlns_delimiter(bytes_str[end]) {
261 end += 1;
262 }
263 end
264}
265
266fn is_xmlns_delimiter(b: u8) -> bool {
267 b == b'=' || b == b' ' || b == b'\t' || b == b'\n' || b == b'/' || b == b'>'
268}
269
270fn scan_used_prefixes(
271 text: &str,
272 declared: &std::collections::HashSet<String>,
273) -> std::collections::HashSet<String> {
274 let bytes_str = text.as_bytes();
275 let mut used = std::collections::HashSet::new();
276 let mut j = 0;
277 while j < bytes_str.len() {
278 if is_prefix_scan_boundary(bytes_str[j]) {
279 match extract_prefix_at(bytes_str, text, j) {
280 Some((prefix, next)) => {
281 if !declared.contains(prefix) {
282 used.insert(prefix.to_string());
283 }
284 j = next;
285 }
286 None => j += 1,
287 }
288 } else {
289 j += 1;
290 }
291 }
292 used
293}
294
295fn is_prefix_scan_boundary(b: u8) -> bool {
296 b == b'<' || b == b' ' || b == b'\t' || b == b'\n'
297}
298
299fn extract_prefix_at<'a>(bytes_str: &[u8], text: &'a str, j: usize) -> Option<(&'a str, usize)> {
300 let ident_start = prefix_ident_start(bytes_str, j);
301 let end = prefix_ident_end(bytes_str, ident_start);
302 if end > ident_start && end < bytes_str.len() && bytes_str[end] == b':' {
303 Some((&text[ident_start..end], end))
304 } else {
305 None
306 }
307}
308
309fn prefix_ident_start(bytes_str: &[u8], j: usize) -> usize {
310 let mut k = j + 1;
311 if k < bytes_str.len() && bytes_str[k] == b'/' {
312 k += 1;
313 }
314 k
315}
316
317fn prefix_ident_end(bytes_str: &[u8], mut k: usize) -> usize {
318 while k < bytes_str.len() && is_prefix_ident_byte(bytes_str[k]) {
319 k += 1;
320 }
321 k
322}
323
324fn is_prefix_ident_byte(c: u8) -> bool {
325 c.is_ascii_alphanumeric() || c == b'_' || c == b'-'
326}