zune_hdr/encoder.rs
1/*
2 * Copyright (c) 2023.
3 *
4 * This software is free software; You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license
5 */
6
7//! Radiance HDR encoder
8
9use alloc::{format, vec};
10use std::collections::HashMap;
11
12use zune_core::bytestream::{ZByteIoError, ZByteWriterTrait, ZWriter};
13use zune_core::colorspace::ColorSpace;
14use zune_core::options::EncoderOptions;
15
16use crate::errors::HdrEncodeErrors;
17
18/// A simple HDR encoder
19///
20/// Data is expected to be in `f32` and its size should be
21/// `width*height*3`
22pub struct HdrEncoder<'a> {
23 data: &'a [f32],
24 headers: Option<&'a HashMap<String, String>>,
25 options: EncoderOptions
26}
27
28impl<'a> HdrEncoder<'a> {
29 /// Create a new HDR encoder context that can encode
30 /// the provided data
31 ///
32 /// # Arguments
33 /// - `data`: Data to encode
34 /// - `options`: Contains metadata for data, including width and height
35 pub fn new(data: &'a [f32], options: EncoderOptions) -> HdrEncoder<'a> {
36 Self {
37 data,
38 headers: None,
39 options
40 }
41 }
42 /// Add extra headers to be encoded with the image
43 ///
44 /// This must be called before you call [`encode`](crate::encoder::HdrEncoder::encode)
45 /// otherwise it will have no effect.
46 ///
47 /// # Arguments:
48 /// - headers: A hashmap containing keys and values, the values will be encoded as key=value
49 /// in the hdr header before encoding
50 pub fn add_headers(&mut self, headers: &'a HashMap<String, String>) {
51 self.headers = Some(headers)
52 }
53
54 /// Calculate buffer with padding size needed for
55 /// encoding this into a vec
56 ///
57 /// This is a given upper limit and doesn't specifically mean that
58 /// your buffer should exactly be that size.
59 ///
60 /// The size of the output will depend on the nature of your data
61 pub fn expected_buffer_size(&self) -> Option<usize> {
62 self.options
63 .width()
64 .checked_mul(self.options.height())?
65 .checked_mul(4)?
66 .checked_add(1024)
67 }
68
69 /// Encode into a sink
70 ///
71 /// The encoder expects colorspace to be in RGB and the length to match
72 ///
73 /// otherwise it's an error to try and encode
74 ///
75 /// The floating point data is expected to be normalized between 0.0 and 1.0, it will not be clipped
76 /// if not in this range
77 ///
78 /// The size of output cannot be determined up until compression is over hence
79 /// we cannot be sure if the output buffer will be enough.
80 ///
81 /// The library provides [expected_buffer_size](crate::HdrEncoder::expected_buffer_size) which
82 /// guarantees that if your buffer is that big, encoding will always succeed
83 ///
84 /// # Arguments:
85 /// - out: The output buffer to write bytes into
86 ///
87 /// # Returns
88 /// - Ok(usize): The number of bytes written into out
89 /// - Err(HdrEncodeErrors): An error if something occurred
90 ///
91 /// # Examples
92 /// - Encode a black image of 10x10
93 ///```
94 /// use zune_core::bit_depth::BitDepth;
95 /// use zune_core::colorspace::ColorSpace;
96 /// use zune_core::options::EncoderOptions;
97 /// use zune_hdr::HdrEncoder;
98 /// let w = 10;
99 /// let h = 10;
100 /// let comp = 3;
101 /// let data = vec![0.0_f32;w*h*comp];
102 /// let opts = EncoderOptions::new(w,h,ColorSpace::RGB,BitDepth::Float32);
103 /// let encoder = HdrEncoder::new(&data,opts);
104 /// // create output buffer , this is the upper limit on it
105 /// let mut output = Vec::with_capacity(encoder.expected_buffer_size().unwrap());
106 /// let size = encoder.encode(&mut output).unwrap();
107 ///```
108 ///
109 /// - Encode but directly write to a file
110 ///```no_run
111 /// use std::fs::OpenOptions;
112 /// use std::io::{BufReader, BufWriter};
113 /// use zune_core::bit_depth::BitDepth;
114 /// use zune_core::colorspace::ColorSpace;
115 /// use zune_core::options::EncoderOptions;
116 /// use zune_hdr::HdrEncoder;
117 /// let w = 10;
118 /// let h = 10;
119 /// let comp = 3;
120 /// let data = vec![0.0_f32;w*h*comp];
121 /// let opts = EncoderOptions::new(w,h,ColorSpace::RGB,BitDepth::Float32);
122 /// let encoder = HdrEncoder::new(&data,opts);
123 /// // create output buffer , this is the upper limit on it
124 /// let mut output = OpenOptions::new().create(true).write(true).truncate(true).open("./black.hdr").unwrap();
125 /// let mut buffered_output = BufWriter::new(output);
126 /// let size = encoder.encode(&mut buffered_output).unwrap();
127 ///
128 /// ```
129 pub fn encode<T: ZByteWriterTrait>(&self, out: T) -> Result<usize, HdrEncodeErrors> {
130 let expected = self
131 .options
132 .width()
133 .checked_mul(self.options.height())
134 .ok_or(HdrEncodeErrors::Static("overflow detected"))?
135 .checked_mul(3)
136 .ok_or(HdrEncodeErrors::Static("overflow detected"))?;
137
138 let found = self.data.len();
139
140 if expected != found {
141 return Err(HdrEncodeErrors::WrongInputSize(expected, found));
142 }
143 if self.options.colorspace() != ColorSpace::RGB {
144 return Err(HdrEncodeErrors::UnsupportedColorspace(
145 self.options.colorspace()
146 ));
147 }
148 let mut writer = ZWriter::new(out);
149 // reserve space
150 let size = self
151 .expected_buffer_size()
152 .ok_or(HdrEncodeErrors::Static("overflow detected"))?;
153 writer.reserve(size)?;
154 // write headers
155 {
156 writer.write_all(b"#?RADIANCE\n")?;
157 writer.write_all(b"SOFTWARE=zune-hdr\n")?;
158 if let Some(headers) = self.headers {
159 for (k, v) in headers {
160 writer.write_all(format!("{}={}\n", k, v).as_bytes())?;
161 }
162 }
163 writer.write_all(b"FORMAT=32-bit_rle_rgbe\n\n")?;
164
165 // write lengths
166 let length_format =
167 format!("-Y {} +X {}\n", self.options.height(), self.options.width());
168
169 writer.write_all(length_format.as_bytes())?;
170 }
171 let width = self.options.width();
172
173 let scanline_stride = width * 3;
174
175 let mut in_scanline = vec![0_u8; width * 4]; // RGBE
176
177 for scanline in self.data.chunks_exact(scanline_stride) {
178 if !(8..=0x7fff).contains(&width) {
179 for (pixels, out) in scanline
180 .chunks_exact(3)
181 .zip(in_scanline.chunks_exact_mut(4))
182 {
183 float_to_rgbe(pixels.try_into().unwrap(), out.try_into().unwrap());
184 }
185 writer.write_all(&in_scanline)?;
186 } else {
187 let bytes = [2, 2, (width >> 8) as u8, (width & 255) as u8];
188 writer.write_const_bytes(&bytes)?;
189
190 for (pixels, out) in scanline
191 .chunks_exact(3)
192 .zip(in_scanline.chunks_exact_mut(4))
193 {
194 float_to_rgbe(pixels.try_into().unwrap(), out.try_into().unwrap());
195 }
196 for i in 0..4 {
197 rle(&in_scanline[i..], &mut writer, width)?;
198 }
199 }
200 }
201 // truncate position to where we reached
202 let position = writer.bytes_written();
203 Ok(position)
204 }
205}
206
207fn rle<T: ZByteWriterTrait>(
208 data: &[u8], writer: &mut ZWriter<T>, width: usize
209) -> Result<(), ZByteIoError> {
210 const MIN_RLE: usize = 4;
211 let mut cur = 0;
212
213 while cur < width {
214 let mut run_count = 0;
215 let mut old_run_count = 0;
216 let mut beg_run = cur;
217 let mut buf: [u8; 2] = [0; 2];
218
219 while run_count < MIN_RLE && beg_run < width {
220 beg_run += run_count;
221 old_run_count = run_count;
222 run_count = 1;
223
224 while (beg_run + run_count < width)
225 && (run_count < 127)
226 && (data[beg_run * 4] == data[(beg_run + run_count) * 4])
227 {
228 run_count += 1;
229 }
230 }
231
232 if (old_run_count > 1) && (old_run_count == beg_run - cur) {
233 buf[0] = (128 + old_run_count) as u8;
234 buf[1] = data[cur * 4];
235 writer.write_all(&buf)?;
236 cur = beg_run;
237 }
238
239 while cur < beg_run {
240 let nonrun_count = 128.min(beg_run - cur);
241 buf[0] = nonrun_count as u8;
242 writer.write_u8(buf[0]);
243 for i in 0..nonrun_count {
244 writer.write_u8_err(data[(cur + i) * 4])?;
245 }
246
247 cur += nonrun_count;
248 }
249
250 if run_count >= MIN_RLE {
251 buf[0] = (128 + run_count) as u8;
252 buf[1] = data[beg_run * 4];
253 writer.write_all(&buf)?;
254 cur += run_count;
255 }
256 }
257 Ok(())
258}
259
260fn float_to_rgbe(rgb: &[f32; 3], rgbe: &mut [u8; 4]) {
261 let v = rgb.iter().fold(f32::MIN, |x, y| x.max(*y));
262
263 if v > 1e-32 {
264 let old_v = v;
265 let (mut v, e) = frexp(v);
266 v = v * 256. / old_v;
267 rgbe[0] = (rgb[0] * v).clamp(0.0, 255.0) as u8;
268 rgbe[1] = (rgb[1] * v).clamp(0.0, 255.0) as u8;
269 rgbe[2] = (rgb[2] * v).clamp(0.0, 255.0) as u8;
270
271 rgbe[3] = (e.wrapping_add(128)) as u8;
272 } else {
273 rgbe.fill(0);
274 }
275}
276
277#[rustfmt::skip]
278pub fn abs(num: f32) -> f32
279{
280 // standard compliant
281 // handles NAN and infinity.
282 // pretty cool
283 f32::from_bits(num.to_bits() & (i32::MAX as u32))
284}
285
286/// Implementation of signum that works in no_std
287#[rustfmt::skip]
288pub fn signum(num: f32) -> f32
289{
290 if num.is_nan() { f32::NAN } else if num.is_infinite()
291 {
292 if num.is_sign_positive() { 0.0 } else { -0.0 }
293 } else if num > 0.0 { 1.0 } else { -1.0 }
294}
295
296fn floor(num: f32) -> f32 {
297 if num.is_nan() || num.is_infinite() {
298 /* handle infinities and nan */
299 return num;
300 }
301 let n = num as u64;
302 let d = n as f32;
303
304 if d == num || num >= 0.0 {
305 d
306 } else {
307 d - 1.0
308 }
309}
310
311/// Fast log2 approximation
312/// (we really don't need that accurate)
313#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
314fn fast_log2(x: f32) -> f32 {
315 /*
316 * Fast log approximation from
317 * https://github.com/romeric/fastapprox
318 *
319 * Some pretty good stuff.
320 */
321 let vx = x.to_bits();
322 let mx = (vx & 0x007F_FFFF) | 0x3f00_0000;
323 let mx_f = f32::from_bits(mx);
324
325 let mut y = vx as f32;
326 // 1/(1<<23)
327 y *= 1.192_092_9e-7;
328
329 y - 124.225_52 - 1.498_030_3 * mx_f - 1.725_88 / (0.352_088_72 + mx_f)
330}
331
332/// non standard frexp implementation
333fn frexp(s: f32) -> (f32, i32) {
334 // from https://stackoverflow.com/a/55696477
335 if 0.0 == s {
336 (s, 0)
337 } else {
338 let lg = fast_log2(abs(s));
339 let lg_floor = floor(lg);
340 // Note: This is the only reason we need the standard library
341 // I haven't found a good exp2 function, fast_exp2 doesn't work
342 // and libm/musl exp2 introduces visible color distortions and is slow, so for
343 // now let's stick to whatever the platform provides
344 let x = (lg - lg_floor - 1.0).exp2();
345 let exp = lg_floor + 1.0;
346 (signum(s) * x, exp as i32)
347 }
348}