Skip to main content

mimick/library/texture_decode/
codecs.rs

1pub(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}