1use std::path::{Path, PathBuf};
18
19use mos_core::{Diagnostic, DiagnosticAnnotation, SourceSpan, codes};
20
21#[derive(Debug, Clone)]
24pub(crate) struct DecodedImage {
25 pub width: u32,
27 pub height: u32,
29 pub rgb8: Vec<u8>,
33}
34
35pub(crate) fn load(
42 src_path: &str,
43 source_file: &Path,
44 call_span: &SourceSpan,
45) -> Result<(PathBuf, DecodedImage), Box<Diagnostic>> {
46 let resolved = mos_core::resolve_source_path(src_path, source_file).map_err(|err| {
47 Box::new(
48 Diagnostic::simple(
49 &codes::MOS0049,
50 None,
51 format!("cannot use image path `{src_path}`: {err}"),
52 )
53 .with_span(call_span.clone()),
54 )
55 })?;
56 let bytes = std::fs::read(&resolved).map_err(|err| {
57 Box::new(
58 Diagnostic::simple(
59 &codes::MOS0012,
60 None,
61 format!(
62 "cannot read image `{}`: {err}",
63 mos_core::display_path(&resolved)
64 ),
65 )
66 .with_span(call_span.clone()),
67 )
68 })?;
69 let decoded = decode(&bytes).map_err(|err| {
70 Box::new(
71 Diagnostic::simple(
72 &codes::MOS0029,
73 None,
74 format!(
75 "cannot decode `{}`: {err}",
76 mos_core::display_path(&resolved)
77 ),
78 )
79 .with_span(call_span.clone())
80 .with_annotation(DiagnosticAnnotation::Note(
81 "supported formats are PNG and JPEG".to_owned(),
82 )),
83 )
84 })?;
85 Ok((resolved, decoded))
86}
87
88#[allow(
89 clippy::many_single_char_names,
90 reason = "r/g/b/a are conventional pixel channel names"
91)]
92fn decode(bytes: &[u8]) -> Result<DecodedImage, String> {
93 let img = image::load_from_memory(bytes).map_err(|err| err.to_string())?;
98 let rgba = img.to_rgba8();
99 let (w, h) = rgba.dimensions();
100 let pixels = rgba.into_raw();
101 let pixel_count = pixels.len() / 4;
107 let mut rgb8 = Vec::with_capacity(pixel_count * 3);
108 for chunk in pixels.chunks_exact(4) {
109 let [r, g, b, a] = [chunk[0], chunk[1], chunk[2], chunk[3]];
110 if a == 255 {
111 rgb8.push(r);
112 rgb8.push(g);
113 rgb8.push(b);
114 } else if a == 0 {
115 rgb8.extend_from_slice(&[255, 255, 255]);
116 } else {
117 rgb8.push(composite(r, a));
118 rgb8.push(composite(g, a));
119 rgb8.push(composite(b, a));
120 }
121 }
122 Ok(DecodedImage {
123 width: w,
124 height: h,
125 rgb8,
126 })
127}
128
129fn composite(channel: u8, alpha: u8) -> u8 {
132 let c = u32::from(channel);
133 let a = u32::from(alpha);
134 let v = (c * a + 255 * (255 - a) + 127) / 255;
137 u8::try_from(v.min(255)).unwrap_or(255)
138}
139
140#[cfg(test)]
141mod tests {
142 #![allow(clippy::unwrap_used, clippy::expect_used)]
143
144 use super::*;
145
146 fn red_blue_png() -> Vec<u8> {
149 let mut buf = image::RgbaImage::new(2, 1);
152 buf.put_pixel(0, 0, image::Rgba([255, 0, 0, 255]));
153 buf.put_pixel(1, 0, image::Rgba([0, 0, 255, 255]));
154 let mut out = std::io::Cursor::new(Vec::new());
155 image::DynamicImage::ImageRgba8(buf)
156 .write_to(&mut out, image::ImageFormat::Png)
157 .unwrap();
158 out.into_inner()
159 }
160
161 #[test]
162 fn decode_opaque_png_round_trips_dimensions_and_colors() {
163 let png = red_blue_png();
164 let img = decode(&png).unwrap();
165 assert_eq!((img.width, img.height), (2, 1));
166 assert_eq!(img.rgb8, vec![255, 0, 0, 0, 0, 255]);
167 }
168
169 #[test]
170 fn decode_transparent_pixel_composites_to_white() {
171 let mut buf = image::RgbaImage::new(1, 1);
172 buf.put_pixel(0, 0, image::Rgba([0, 128, 255, 0]));
173 let mut out = std::io::Cursor::new(Vec::new());
174 image::DynamicImage::ImageRgba8(buf)
175 .write_to(&mut out, image::ImageFormat::Png)
176 .unwrap();
177 let img = decode(&out.into_inner()).unwrap();
178 assert_eq!(img.rgb8, vec![255, 255, 255]);
179 }
180
181 #[test]
182 fn decode_partial_alpha_composites_against_white() {
183 let mut buf = image::RgbaImage::new(1, 1);
185 buf.put_pixel(0, 0, image::Rgba([255, 0, 0, 128]));
186 let mut out = std::io::Cursor::new(Vec::new());
187 image::DynamicImage::ImageRgba8(buf)
188 .write_to(&mut out, image::ImageFormat::Png)
189 .unwrap();
190 let img = decode(&out.into_inner()).unwrap();
191 assert_eq!(img.rgb8[0], 255);
195 assert!((i32::from(img.rgb8[1]) - 127).abs() <= 1);
196 assert!((i32::from(img.rgb8[2]) - 127).abs() <= 1);
197 }
198
199 #[test]
200 fn decode_bogus_bytes_returns_error() {
201 let r = decode(b"not an image at all");
202 assert!(r.is_err());
203 }
204
205 #[test]
206 fn resolve_relative_path_uses_source_parent() {
207 let src = Path::new("/tmp/proj/main.mos");
208 let resolved = mos_core::resolve_source_path("img/scan.png", src);
209 assert_eq!(resolved, Ok(PathBuf::from("/tmp/proj/img/scan.png")));
210 }
211
212 #[test]
213 fn resolve_absolute_path_passes_through() {
214 let src = Path::new("/tmp/proj/main.mos");
215 let resolved = mos_core::resolve_source_path("/abs/path.png", src);
216 assert_eq!(resolved, Ok(PathBuf::from("/abs/path.png")));
217 }
218}