1use otf_pixels_core::{
11 ChannelLayout, ImageDescriptor, Op, PixelFormat, PixelsError, Region, Result, SampleKind, Tile,
12 TileMut,
13};
14use std::sync::Arc;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct ConvertFormat {
19 to: PixelFormat,
20}
21
22impl ConvertFormat {
23 #[must_use]
25 pub const fn to(to: PixelFormat) -> Self {
26 Self { to }
27 }
28}
29
30fn read(pixel: &[u8], kind: SampleKind, index: usize) -> f32 {
32 match kind {
33 SampleKind::U8 => f32::from(pixel.get(index).copied().unwrap_or(0)) / 255.0,
34 SampleKind::U16 => {
35 let at = index * 2;
36 let v = u16::from_ne_bytes([
37 pixel.get(at).copied().unwrap_or(0),
38 pixel.get(at + 1).copied().unwrap_or(0),
39 ]);
40 f32::from(v) / 65535.0
41 }
42 SampleKind::F32 => {
43 let at = index * 4;
44 let mut b = [0_u8; 4];
45 if let Some(s) = pixel.get(at..at + 4) {
46 b.copy_from_slice(s);
47 }
48 f32::from_ne_bytes(b)
49 }
50 }
51}
52
53fn write(pixel: &mut [u8], kind: SampleKind, index: usize, value: f32) {
54 match kind {
55 SampleKind::U8 => {
56 if let Some(slot) = pixel.get_mut(index) {
57 *slot = (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
58 }
59 }
60 SampleKind::U16 => {
61 let v = (value.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
62 if let Some(slot) = pixel.get_mut(index * 2..index * 2 + 2) {
63 slot.copy_from_slice(&v.to_ne_bytes());
64 }
65 }
66 SampleKind::F32 => {
67 if let Some(slot) = pixel.get_mut(index * 4..index * 4 + 4) {
68 slot.copy_from_slice(&value.to_ne_bytes());
69 }
70 }
71 }
72}
73
74fn to_rgba(pixel: &[u8], format: PixelFormat) -> [f32; 4] {
76 let kind = format.sample_kind();
77 let at = |i| read(pixel, kind, i);
78 match format.layout() {
79 ChannelLayout::Gray => [at(0), at(0), at(0), 1.0],
80 ChannelLayout::GrayAlpha => [at(0), at(0), at(0), at(1)],
81 ChannelLayout::Rgb => [at(0), at(1), at(2), 1.0],
82 ChannelLayout::Rgba => [at(0), at(1), at(2), at(3)],
83 }
84}
85
86fn from_rgba(rgba: [f32; 4], pixel: &mut [u8], format: PixelFormat) {
87 let kind = format.sample_kind();
88 let [r, g, b, a] = rgba;
89 let luma = || 0.299 * r + 0.587 * g + 0.114 * b;
90 match format.layout() {
91 ChannelLayout::Gray => write(pixel, kind, 0, luma()),
92 ChannelLayout::GrayAlpha => {
93 write(pixel, kind, 0, luma());
94 write(pixel, kind, 1, a);
95 }
96 ChannelLayout::Rgb => {
97 for (i, v) in [r, g, b].into_iter().enumerate() {
98 write(pixel, kind, i, v);
99 }
100 }
101 ChannelLayout::Rgba => {
102 for (i, v) in [r, g, b, a].into_iter().enumerate() {
103 write(pixel, kind, i, v);
104 }
105 }
106 }
107}
108
109fn narrow_16_to_8(v: u16) -> u8 {
112 ((u32::from(v) * 255 + 32_767) / 65_535) as u8
113}
114
115impl Op for ConvertFormat {
116 fn name(&self) -> &'static str {
117 "convert_format"
118 }
119
120 fn rescaled(&self) -> Option<Arc<dyn Op>> {
122 Some(Arc::new(*self))
123 }
124
125 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
126 let [input] = inputs else {
127 return Err(PixelsError::graph("convert_format takes exactly one input"));
128 };
129 ImageDescriptor::new(input.width, input.height, self.to)
130 }
131
132 fn input_regions(&self, output: Region, _inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
133 Ok(vec![output])
134 }
135
136 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
137 let [input] = inputs else {
138 return Err(PixelsError::graph("convert_format takes exactly one input"));
139 };
140 let (from, to) = (input.pixel(), self.to);
141 let (in_bytes, out_bytes) = (from.bytes_per_pixel(), to.bytes_per_pixel());
142 let region = output.region();
143 let same_layout = from.layout() == to.layout();
144 for y in region.y..region.y + region.height {
145 let (Some(source), Some(target)) = (input.row(y), output.row_mut(y)) else {
146 continue;
147 };
148 if from == to {
149 let len = source.len().min(target.len());
150 if let (Some(t), Some(s)) = (target.get_mut(..len), source.get(..len)) {
151 t.copy_from_slice(s);
152 }
153 continue;
154 }
155 if same_layout
156 && from.sample_kind() == SampleKind::U16
157 && to.sample_kind() == SampleKind::U8
158 {
159 for (pair, slot) in source.chunks_exact(2).zip(target.iter_mut()) {
161 if let &[lo, hi] = pair {
162 *slot = narrow_16_to_8(u16::from_ne_bytes([lo, hi]));
163 }
164 }
165 continue;
166 }
167 for (from_px, to_px) in source
168 .chunks_exact(in_bytes)
169 .zip(target.chunks_exact_mut(out_bytes))
170 {
171 from_rgba(to_rgba(from_px, from), to_px, to);
172 }
173 }
174 Ok(())
175 }
176}
177
178#[cfg(test)]
179#[allow(
180 clippy::unwrap_used,
181 clippy::indexing_slicing,
182 reason = "tests operate on known-good values"
183)]
184mod tests {
185 use super::*;
186 use otf_pixels_core::TileBuf;
187
188 fn convert(from: PixelFormat, bytes: Vec<u8>, to: PixelFormat) -> Vec<u8> {
189 let pixels = bytes.len() / from.bytes_per_pixel();
190 let input = ImageDescriptor::new(pixels as u32, 1, from).unwrap();
191 let op = ConvertFormat::to(to);
192 let out = op.output_descriptor(&[input]).unwrap();
193 let source = TileBuf::from_vec(input.region(), from, bytes).unwrap();
194 let mut target = TileBuf::for_image(&out).unwrap();
195 op.compute(
196 &[source.as_tile().unwrap()],
197 &mut target.as_tile_mut().unwrap(),
198 )
199 .unwrap();
200 target.into_bytes()
201 }
202
203 fn wide(values: &[u16]) -> Vec<u8> {
204 values.iter().flat_map(|v| v.to_ne_bytes()).collect()
205 }
206
207 #[test]
208 fn depth_rescales_with_rounding() {
209 assert_eq!(
210 convert(
211 PixelFormat::Gray16,
212 wide(&[0, 128, 32_896, 65_535]),
213 PixelFormat::Gray8
214 ),
215 vec![0, 0, 128, 255]
216 );
217 let all: Vec<u8> = (0..=255).collect();
219 let up = convert(PixelFormat::Gray8, all.clone(), PixelFormat::Gray16);
220 assert_eq!(up[2..4], 257_u16.to_ne_bytes());
221 assert_eq!(convert(PixelFormat::Gray16, up, PixelFormat::Gray8), all);
222 let floats: Vec<u8> = [-0.5_f32, 0.5, 2.0]
224 .iter()
225 .flat_map(|v| v.to_ne_bytes())
226 .collect();
227 assert_eq!(
228 convert(PixelFormat::RgbF32, floats, PixelFormat::Rgb8),
229 vec![0, 128, 255]
230 );
231 }
232
233 #[test]
234 fn layouts_widen_narrow_and_add_or_drop_alpha() {
235 assert_eq!(
236 convert(PixelFormat::Gray8, vec![7], PixelFormat::Rgba8),
237 vec![7, 7, 7, 255]
238 );
239 assert_eq!(
240 convert(PixelFormat::Rgb8, vec![255, 0, 0], PixelFormat::Gray8),
241 vec![76]
242 );
243 assert_eq!(
244 convert(PixelFormat::Rgba8, vec![1, 2, 3, 4], PixelFormat::Rgb8),
245 vec![1, 2, 3]
246 );
247 assert_eq!(
248 convert(PixelFormat::GrayA8, vec![9, 100], PixelFormat::Rgba16),
249 wide(&[9 * 257, 9 * 257, 9 * 257, 100 * 257])
250 );
251 assert_eq!(
252 convert(
253 PixelFormat::Rgba16,
254 wide(&[65_535, 0, 0, 32_768]),
255 PixelFormat::GrayA8
256 ),
257 vec![76, 128]
258 );
259 }
260}