/rust/registry/src/index.crates.io-1949cf8c6b5b557f/exr-1.74.2/src/compression/mod.rs
Line | Count | Source |
1 | | //! Contains the compression attribute definition |
2 | | //! and methods to compress and decompress data. |
3 | | |
4 | | // private modules make non-breaking changes easier |
5 | | mod b44; |
6 | | |
7 | | // public only for benchmarking |
8 | | #[doc(hidden)] |
9 | | pub mod dwa; |
10 | | |
11 | | mod piz; |
12 | | mod pxr24; |
13 | | mod rle; |
14 | | mod zip; |
15 | | |
16 | | use std::convert::TryInto; |
17 | | |
18 | | use crate::{ |
19 | | error::{usize_to_i32, Error, Result, UnitResult}, |
20 | | meta::{ |
21 | | attribute::{ChannelList, IntegerBounds, SampleType}, |
22 | | header::Header, |
23 | | }, |
24 | | }; |
25 | | |
26 | | /// A byte vector. |
27 | | pub type ByteVec = Vec<u8>; |
28 | | |
29 | | /// A byte slice. |
30 | | pub type Bytes<'s> = &'s [u8]; |
31 | | |
32 | | /// Specifies which compression method to use. |
33 | | /// |
34 | | /// Use uncompressed data for fastest loading and writing speeds. |
35 | | /// Use RLE compression for fast loading and writing with slight memory savings. |
36 | | /// Use ZIP compression for slow processing with large memory savings. |
37 | | #[derive(Debug, Clone, Copy, PartialEq)] |
38 | | pub enum Compression { |
39 | | /// Store uncompressed values. |
40 | | /// Produces large files that can be read and written very quickly. |
41 | | /// Consider using RLE instead, as it provides some compression with almost |
42 | | /// equivalent speed. |
43 | | Uncompressed, |
44 | | |
45 | | /// Produces slightly smaller files |
46 | | /// that can still be read and written rather quickly. |
47 | | /// The compressed file size is usually between 60 and 75 percent of the |
48 | | /// uncompressed size. Works best for images with large flat areas, such |
49 | | /// as masks and abstract graphics. This compression method is lossless. |
50 | | RLE, |
51 | | |
52 | | /// Uses ZIP compression to compress each line. Slowly produces small images |
53 | | /// which can be read with moderate speed. This compression method is |
54 | | /// lossless. Might be slightly faster but larger than `ZIP16´. |
55 | | ZIP1, /* TODO ZIP { individual_lines: bool, compression_level: Option<u8> } // TODO |
56 | | * specify zip compression level? */ |
57 | | /// Uses ZIP compression to compress blocks of 16 lines. Slowly produces |
58 | | /// small images which can be read with moderate speed. This compression |
59 | | /// method is lossless. Might be slightly slower but smaller than |
60 | | /// `ZIP1´. |
61 | | ZIP16, // TODO collapse with ZIP1 |
62 | | |
63 | | /// PIZ compression works well for noisy and natural images. Works better |
64 | | /// with larger tiles. Only supported for flat images, but not for deep |
65 | | /// data. This compression method is lossless. |
66 | | // A wavelet transform is applied to the pixel data, and the result is Huffman- |
67 | | // encoded. This scheme tends to provide the best compression ratio for the types of |
68 | | // images that are typically processed at Industrial Light & Magic. Files are |
69 | | // compressed and decompressed at roughly the same speed. For photographic |
70 | | // images with film grain, the files are reduced to between 35 and 55 percent of their |
71 | | // uncompressed size. |
72 | | // PIZ compression works well for scan-line based files, and also for tiled files with |
73 | | // large tiles, but small tiles do not shrink much. (PIZ-compressed data start with a |
74 | | // relatively long header; if the input to the compressor is short, adding the header |
75 | | // tends to offset any size reduction of the input.) |
76 | | PIZ, |
77 | | |
78 | | /// Like `ZIP1`, but reduces precision of `f32` images to `f24`. |
79 | | /// Therefore, this is lossless compression for `f16` and `u32` data, lossy |
80 | | /// compression for `f32` data. This compression method works well for |
81 | | /// depth buffers and similar images, where the possible range of values |
82 | | /// is very large, but where full 32-bit floating-point accuracy is not |
83 | | /// necessary. Rounding improves compression significantly by |
84 | | /// eliminating the pixels' 8 least significant bits, which tend to be |
85 | | /// very noisy, and therefore difficult to compress. This produces |
86 | | /// really small image files. Only supported for flat images, not for deep |
87 | | /// data. |
88 | | // After reducing 32-bit floating-point data to 24 bits by rounding (while leaving 16-bit |
89 | | // floating-point data unchanged), differences between horizontally adjacent pixels |
90 | | // are compressed with zlib, similar to ZIP. PXR24 compression preserves image |
91 | | // channels of type HALF and UINT exactly, but the relative error of FLOAT data |
92 | | // increases to about ???. |
93 | | PXR24, // TODO specify zip compression level? |
94 | | |
95 | | /// This is a lossy compression method for f16 images. |
96 | | /// It's the predecessor of the `B44A` compression, |
97 | | /// which has improved compression rates for uniformly colored areas. |
98 | | /// You should probably use `B44A` instead of the plain `B44`. |
99 | | /// |
100 | | /// Only supported for flat images, not for deep data. |
101 | | // lossy 4-by-4 pixel block compression, |
102 | | // flat fields are compressed more |
103 | | // Channels of type HALF are split into blocks of four by four pixels or 32 bytes. Each |
104 | | // block is then packed into 14 bytes, reducing the data to 44 percent of their |
105 | | // uncompressed size. When B44 compression is applied to RGB images in |
106 | | // combination with luminance/chroma encoding (see below), the size of the |
107 | | // compressed pixels is about 22 percent of the size of the original RGB data. |
108 | | // Channels of type UINT or FLOAT are not compressed. |
109 | | // Decoding is fast enough to allow real-time playback of B44-compressed OpenEXR |
110 | | // image sequences on commodity hardware. |
111 | | // The size of a B44-compressed file depends on the number of pixels in the image, |
112 | | // but not on the data in the pixels. All images with the same resolution and the same |
113 | | // set of channels have the same size. This can be advantageous for systems that |
114 | | // support real-time playback of image sequences; the predictable file size makes it |
115 | | // easier to allocate space on storage media efficiently. |
116 | | // B44 compression is only supported for flat images. |
117 | | B44, // TODO B44 { optimize_uniform_areas: bool } |
118 | | |
119 | | /// This is a lossy compression method for f16 images. |
120 | | /// All f32 and u32 channels will be stored without compression. |
121 | | /// All the f16 pixels are divided into 4x4 blocks. |
122 | | /// Each block is then compressed as a whole. |
123 | | /// |
124 | | /// The 32 bytes of a block will require only ~14 bytes after compression, |
125 | | /// independent of the actual pixel contents. With chroma subsampling, |
126 | | /// a block will be compressed to ~7 bytes. |
127 | | /// Uniformly colored blocks will be compressed to ~3 bytes. |
128 | | /// |
129 | | /// The 512 bytes of an f32 block will not be compressed at all. |
130 | | /// |
131 | | /// Should be fast enough for realtime playback. |
132 | | /// Only supported for flat images, not for deep data. |
133 | | B44A, // TODO collapse with B44 |
134 | | |
135 | | /// Lossy DCT-based compression (DreamWorks Animation), 32 scanlines per |
136 | | /// block. Partial buffer access friendly. |
137 | | /// Decoding support is implemented. Encoding is not implemented and returns |
138 | | /// an error. |
139 | | DWAA(Option<f32>), |
140 | | |
141 | | /// Lossy DCT-based compression (DreamWorks Animation), 256 scanlines per |
142 | | /// block. Better compression ratio for full frames. |
143 | | /// Decoding support is implemented. Encoding is not implemented and returns |
144 | | /// an error. |
145 | | DWAB(Option<f32>), |
146 | | |
147 | | /// __This lossy compression is not yet supported by this implementation.__ |
148 | | // High-Throughput JPEG 2000 (32 lines) |
149 | | HTJ2K32, |
150 | | |
151 | | /// __This lossy compression is not yet supported by this implementation.__ |
152 | | // High-Throughput JPEG 2000 (256 lines) |
153 | | HTJ2K256, |
154 | | } |
155 | | |
156 | | impl std::fmt::Display for Compression { |
157 | 0 | fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
158 | 0 | write!( |
159 | 0 | formatter, |
160 | 0 | "{} compression", |
161 | 0 | match self { |
162 | 0 | Self::Uncompressed => "no", |
163 | 0 | Self::RLE => "rle", |
164 | 0 | Self::ZIP1 => "zip line", |
165 | 0 | Self::ZIP16 => "zip block", |
166 | 0 | Self::B44 => "b44", |
167 | 0 | Self::B44A => "b44a", |
168 | 0 | Self::DWAA(_) => "dwaa", |
169 | 0 | Self::DWAB(_) => "dwab", |
170 | 0 | Self::PIZ => "piz", |
171 | 0 | Self::PXR24 => "pxr24", |
172 | 0 | Self::HTJ2K32 => "ht j2k 32", |
173 | 0 | Self::HTJ2K256 => "ht j2k 256", |
174 | | } |
175 | | ) |
176 | 0 | } |
177 | | } |
178 | | |
179 | | impl Compression { |
180 | | /// Compress the image section, converting from native endian into with |
181 | | /// little-endian format. |
182 | 0 | pub fn compress_image_section_to_le( |
183 | 0 | self, |
184 | 0 | header: &Header, |
185 | 0 | uncompressed_native_endian: ByteVec, |
186 | 0 | pixel_section: IntegerBounds, |
187 | 0 | ) -> Result<ByteVec> { |
188 | 0 | let max_tile_size = header.max_block_pixel_size(); |
189 | | |
190 | 0 | assert!( |
191 | 0 | pixel_section.validate(Some(max_tile_size)).is_ok(), |
192 | | "decompress tile coordinate bug" |
193 | | ); |
194 | 0 | if header.deep { |
195 | 0 | assert!(self.supports_deep_data()); |
196 | 0 | } |
197 | | |
198 | | use self::Compression::*; |
199 | 0 | let compressed_little_endian = match self { |
200 | | Uncompressed => { |
201 | 0 | return convert_current_to_little_endian( |
202 | 0 | uncompressed_native_endian, |
203 | 0 | &header.channels, |
204 | 0 | pixel_section, |
205 | | ); |
206 | | } |
207 | | |
208 | | // we need to clone here, because we might have to fallback to the uncompressed data |
209 | | // later (when compressed data is larger than raw data) |
210 | 0 | ZIP16 => zip::compress_bytes( |
211 | 0 | &header.channels, |
212 | 0 | uncompressed_native_endian.clone(), |
213 | 0 | pixel_section, |
214 | | ), |
215 | 0 | ZIP1 => zip::compress_bytes( |
216 | 0 | &header.channels, |
217 | 0 | uncompressed_native_endian.clone(), |
218 | 0 | pixel_section, |
219 | | ), |
220 | 0 | RLE => rle::compress_bytes( |
221 | 0 | &header.channels, |
222 | 0 | uncompressed_native_endian.clone(), |
223 | 0 | pixel_section, |
224 | | ), |
225 | | PIZ => { |
226 | 0 | piz::compress(&header.channels, uncompressed_native_endian.clone(), pixel_section) |
227 | | } |
228 | | PXR24 => { |
229 | 0 | pxr24::compress(&header.channels, uncompressed_native_endian.clone(), pixel_section) |
230 | | } |
231 | 0 | B44 => b44::compress( |
232 | 0 | &header.channels, |
233 | 0 | uncompressed_native_endian.clone(), |
234 | 0 | pixel_section, |
235 | | false, |
236 | | ), |
237 | 0 | B44A => b44::compress( |
238 | 0 | &header.channels, |
239 | 0 | uncompressed_native_endian.clone(), |
240 | 0 | pixel_section, |
241 | | true, |
242 | | ), |
243 | 0 | DWAA(level) | DWAB(level) => dwa::compress( |
244 | 0 | &header.channels, |
245 | 0 | uncompressed_native_endian.clone(), |
246 | 0 | pixel_section, |
247 | 0 | level, |
248 | | ), |
249 | | _ => { |
250 | 0 | return Err(Error::unsupported(format!( |
251 | 0 | "yet unimplemented compression method: {self}" |
252 | 0 | ))); |
253 | | } |
254 | | }; |
255 | | |
256 | 0 | let compressed_little_endian = compressed_little_endian |
257 | 0 | .map_err(|_| Error::invalid(format!("pixels cannot be compressed ({self})")))?; |
258 | | |
259 | 0 | if self == Uncompressed || compressed_little_endian.len() < uncompressed_native_endian.len() |
260 | | { |
261 | | // only write compressed if it actually is smaller than raw |
262 | 0 | Ok(compressed_little_endian) |
263 | | } else { |
264 | | // if we do not use compression, manually convert uncompressed data |
265 | 0 | convert_current_to_little_endian( |
266 | 0 | uncompressed_native_endian, |
267 | 0 | &header.channels, |
268 | 0 | pixel_section, |
269 | | ) |
270 | | } |
271 | 0 | } |
272 | | |
273 | | /// Decompress the image section from bytes of little-endian format, |
274 | | /// returning native-endian format. |
275 | 0 | pub fn decompress_image_section_from_le( |
276 | 0 | self, |
277 | 0 | header: &Header, |
278 | 0 | compressed_le: ByteVec, |
279 | 0 | pixel_section: IntegerBounds, |
280 | 0 | pedantic: bool, |
281 | 0 | ) -> Result<ByteVec> { |
282 | 0 | let max_tile_size = header.max_block_pixel_size(); |
283 | | |
284 | 0 | assert!( |
285 | 0 | pixel_section.validate(Some(max_tile_size)).is_ok(), |
286 | | "decompress tile coordinate bug" |
287 | | ); |
288 | 0 | if header.deep { |
289 | 0 | assert!(self.supports_deep_data()); |
290 | 0 | } |
291 | | |
292 | 0 | let expected_byte_size = pixel_section.size.area() * header.channels.bytes_per_pixel; // FIXME this needs to account for subsampling anywhere |
293 | | |
294 | | // note: always true where self == Uncompressed |
295 | 0 | if compressed_le.len() == expected_byte_size { |
296 | | // the compressed data was larger than the raw data, so the small raw data has |
297 | | // been written |
298 | 0 | convert_little_endian_to_current(compressed_le, &header.channels, pixel_section) |
299 | | } else { |
300 | | use self::Compression::*; |
301 | 0 | let bytes_ne = match self { |
302 | | Uncompressed => { |
303 | 0 | convert_little_endian_to_current(compressed_le, &header.channels, pixel_section) |
304 | | } |
305 | 0 | ZIP16 => zip::decompress_bytes( |
306 | 0 | &header.channels, |
307 | 0 | compressed_le, |
308 | 0 | pixel_section, |
309 | 0 | expected_byte_size, |
310 | 0 | pedantic, |
311 | | ), |
312 | 0 | ZIP1 => zip::decompress_bytes( |
313 | 0 | &header.channels, |
314 | 0 | compressed_le, |
315 | 0 | pixel_section, |
316 | 0 | expected_byte_size, |
317 | 0 | pedantic, |
318 | | ), |
319 | 0 | RLE => rle::decompress_bytes( |
320 | 0 | &header.channels, |
321 | 0 | compressed_le, |
322 | 0 | pixel_section, |
323 | 0 | expected_byte_size, |
324 | 0 | pedantic, |
325 | | ), |
326 | 0 | PIZ => piz::decompress( |
327 | 0 | &header.channels, |
328 | 0 | compressed_le, |
329 | 0 | pixel_section, |
330 | 0 | expected_byte_size, |
331 | 0 | pedantic, |
332 | | ), |
333 | 0 | PXR24 => pxr24::decompress( |
334 | 0 | &header.channels, |
335 | 0 | compressed_le, |
336 | 0 | pixel_section, |
337 | 0 | expected_byte_size, |
338 | 0 | pedantic, |
339 | | ), |
340 | 0 | B44 | B44A => b44::decompress( |
341 | 0 | &header.channels, |
342 | 0 | compressed_le, |
343 | 0 | pixel_section, |
344 | 0 | expected_byte_size, |
345 | 0 | pedantic, |
346 | | ), |
347 | 0 | DWAA(_) | DWAB(_) => dwa::decompress( |
348 | 0 | &header.channels, |
349 | 0 | compressed_le, |
350 | 0 | pixel_section, |
351 | 0 | expected_byte_size, |
352 | 0 | pedantic, |
353 | | ), |
354 | | _ => { |
355 | 0 | return Err(Error::unsupported(format!( |
356 | 0 | "yet unimplemented compression method: {self}" |
357 | 0 | ))); |
358 | | } |
359 | | }; |
360 | | |
361 | | // map all errors to compression errors |
362 | 0 | let bytes_ne = bytes_ne.map_err(|decompression_error| match decompression_error { |
363 | 0 | Error::NotSupported(message) => Error::unsupported(format!( |
364 | 0 | "yet unimplemented compression special case ({message})" |
365 | | )), |
366 | | |
367 | 0 | error => Error::invalid(format!("compressed {self:?} data ({error})")), |
368 | 0 | })?; |
369 | | |
370 | 0 | if bytes_ne.len() == expected_byte_size { |
371 | 0 | Ok(bytes_ne) |
372 | | } else { |
373 | 0 | Err(Error::invalid("decompressed data")) |
374 | | } |
375 | | } |
376 | 0 | } |
377 | | |
378 | | /// For scan line images and deep scan line images, one or more scan lines |
379 | | /// may be stored together as a scan line block. The number of scan |
380 | | /// lines per block depends on how the pixel data are compressed. |
381 | 0 | pub const fn scan_lines_per_block(self) -> usize { |
382 | | use self::Compression::*; |
383 | 0 | match self { |
384 | 0 | Uncompressed | RLE | ZIP1 => 1, |
385 | 0 | ZIP16 | PXR24 => 16, |
386 | 0 | PIZ | B44 | B44A | DWAA(_) | HTJ2K32 => 32, |
387 | 0 | DWAB(_) | HTJ2K256 => 256, |
388 | | } |
389 | 0 | } |
390 | | |
391 | | /// Deep data can only be compressed using RLE or ZIP compression. |
392 | 0 | pub const fn supports_deep_data(self) -> bool { |
393 | | use self::Compression::*; |
394 | 0 | match self { |
395 | 0 | Uncompressed | RLE | ZIP1 => true, |
396 | | |
397 | 0 | ZIP16 | PXR24 | PIZ | B44 | B44A | DWAA(_) | DWAB(_) | HTJ2K256 | HTJ2K32 => false, |
398 | | } |
399 | 0 | } |
400 | | |
401 | | /// Most compression methods will reconstruct the exact pixel bytes, |
402 | | /// but some might throw away unimportant data for specific types of |
403 | | /// samples. |
404 | 0 | pub fn is_lossless_for(self, sample_type: SampleType) -> bool { |
405 | | use self::Compression::*; |
406 | 0 | match self { |
407 | 0 | PXR24 => sample_type != SampleType::F32, // pxr reduces f32 to f24 |
408 | | // B44 only compresses f16 values; other sample types are left |
409 | | // uncompressed. |
410 | 0 | B44 | B44A => sample_type != SampleType::F16, |
411 | 0 | Uncompressed | RLE | ZIP1 | ZIP16 | PIZ | HTJ2K32 | HTJ2K256 => true, |
412 | 0 | DWAB(_) | DWAA(_) => false, |
413 | | } |
414 | 0 | } |
415 | | |
416 | | /// Most compression methods will reconstruct the exact pixel bytes, |
417 | | /// but some might throw away unimportant data in some cases. |
418 | 0 | pub fn may_loose_data(self) -> bool { |
419 | | use self::Compression::*; |
420 | 0 | match self { |
421 | 0 | Uncompressed | RLE | ZIP1 | ZIP16 | PIZ | HTJ2K32 | HTJ2K256 => false, |
422 | 0 | PXR24 | B44 | B44A | DWAB(_) | DWAA(_) => true, |
423 | | } |
424 | 0 | } |
425 | | |
426 | | /// Most compression methods will reconstruct the exact pixel bytes, |
427 | | /// but some might replace NaN with zeroes. |
428 | | /// This might also depend on the sample type of the pixels. |
429 | | /// Even a compression method that supports NaN might change the bit |
430 | | /// patterns of those NaNs. |
431 | 0 | pub fn supports_nan(self) -> bool { |
432 | | use self::Compression::*; |
433 | 0 | match self { |
434 | 0 | B44A | DWAB(_) | DWAA(_) => false, |
435 | 0 | Uncompressed | PXR24 | RLE | ZIP1 | ZIP16 | PIZ | B44 | HTJ2K32 | HTJ2K256 => true, |
436 | | } |
437 | 0 | } |
438 | | |
439 | | /// Most compression methods will reconstruct the exact pixel and NaN bits, |
440 | | /// but some might replace NaN bits with other NaN bits. |
441 | | /// This might also depend on the sample type of the pixels. |
442 | 0 | pub fn preserves_nan_bits(self) -> bool { |
443 | | use self::Compression::*; |
444 | 0 | match self { |
445 | 0 | B44A | PXR24 | DWAB(_) | DWAA(_) => false, |
446 | 0 | B44 | Uncompressed | RLE | ZIP1 | ZIP16 | PIZ | HTJ2K32 | HTJ2K256 => true, |
447 | | } |
448 | 0 | } |
449 | | } |
450 | | |
451 | | // see https://github.com/AcademySoftwareFoundation/openexr/blob/6a9f8af6e89547bcd370ae3cec2b12849eee0b54/OpenEXR/IlmImf/ImfMisc.cpp#L1456-L1541 |
452 | | |
453 | | #[allow(unused)] // allows the extra parameters to be unused |
454 | 0 | fn convert_current_to_little_endian( |
455 | 0 | mut bytes: ByteVec, |
456 | 0 | channels: &ChannelList, |
457 | 0 | rectangle: IntegerBounds, |
458 | 0 | ) -> Result<ByteVec> { |
459 | | #[cfg(target_endian = "big")] |
460 | | reverse_block_endianness(&mut bytes, channels, rectangle)?; |
461 | | |
462 | 0 | Ok(bytes) |
463 | 0 | } |
464 | | |
465 | | #[allow(unused)] // allows the extra parameters to be unused |
466 | 0 | fn convert_little_endian_to_current( |
467 | 0 | mut bytes: ByteVec, |
468 | 0 | channels: &ChannelList, |
469 | 0 | rectangle: IntegerBounds, |
470 | 0 | ) -> Result<ByteVec> { |
471 | | #[cfg(target_endian = "big")] |
472 | | reverse_block_endianness(&mut bytes, channels, rectangle)?; |
473 | | |
474 | 0 | Ok(bytes) |
475 | 0 | } |
476 | | |
477 | | #[allow(unused)] // unused when on little endian system |
478 | 0 | fn reverse_block_endianness( |
479 | 0 | bytes: &mut [u8], |
480 | 0 | channels: &ChannelList, |
481 | 0 | rectangle: IntegerBounds, |
482 | 0 | ) -> UnitResult { |
483 | 0 | let mut remaining_bytes: &mut [u8] = bytes; |
484 | | |
485 | 0 | for y in rectangle.position.y()..rectangle.end().y() { |
486 | 0 | for channel in &channels.list { |
487 | 0 | let line_is_subsampled = mod_p(y, usize_to_i32(channel.sampling.y(), "sampling")?) != 0; |
488 | 0 | if line_is_subsampled { |
489 | 0 | continue; |
490 | 0 | } |
491 | | |
492 | 0 | let sample_count = rectangle.size.width() / channel.sampling.x(); |
493 | | |
494 | 0 | match channel.sample_type { |
495 | 0 | SampleType::F16 => { |
496 | 0 | remaining_bytes = |
497 | 0 | convert_byte_chunks(reverse_2_bytes, 2, remaining_bytes, sample_count); |
498 | 0 | } |
499 | | |
500 | 0 | SampleType::F32 => { |
501 | 0 | remaining_bytes = |
502 | 0 | convert_byte_chunks(reverse_4_bytes, 4, remaining_bytes, sample_count); |
503 | 0 | } |
504 | | |
505 | 0 | SampleType::U32 => { |
506 | 0 | remaining_bytes = |
507 | 0 | convert_byte_chunks(reverse_4_bytes, 4, remaining_bytes, sample_count); |
508 | 0 | } |
509 | | } |
510 | | } |
511 | | } |
512 | | |
513 | | // Converts groups of bytes (e.g. 2 bytes), as many groups as specified. Returns |
514 | | // a slice of the remaining bytes. |
515 | | #[inline] |
516 | 0 | fn convert_byte_chunks( |
517 | 0 | convert_single_value: fn(&mut [u8]), |
518 | 0 | batch_size: usize, |
519 | 0 | bytes: &mut [u8], |
520 | 0 | batch_count: usize, |
521 | 0 | ) -> &mut [u8] { |
522 | 0 | let (line_bytes, rest) = bytes.split_at_mut(batch_count * batch_size); |
523 | 0 | let value_byte_chunks = line_bytes.chunks_exact_mut(batch_size); |
524 | | |
525 | 0 | for value_bytes in value_byte_chunks { |
526 | 0 | convert_single_value(value_bytes); |
527 | 0 | } |
528 | | |
529 | 0 | rest |
530 | 0 | } |
531 | | |
532 | 0 | debug_assert!(remaining_bytes.is_empty(), "not all bytes were converted to little endian"); |
533 | 0 | Ok(()) |
534 | 0 | } |
535 | | |
536 | | #[inline] |
537 | 0 | fn reverse_2_bytes(bytes: &mut [u8]) { |
538 | | // this code seems like it could be optimized easily by the compiler |
539 | 0 | let two_bytes: [u8; 2] = bytes.try_into().expect("invalid byte count"); |
540 | 0 | bytes.copy_from_slice(&[two_bytes[1], two_bytes[0]]); |
541 | 0 | } |
542 | | |
543 | | #[inline] |
544 | 0 | fn reverse_4_bytes(bytes: &mut [u8]) { |
545 | 0 | let four_bytes: [u8; 4] = bytes.try_into().expect("invalid byte count"); |
546 | 0 | bytes.copy_from_slice(&[four_bytes[3], four_bytes[2], four_bytes[1], four_bytes[0]]); |
547 | 0 | } |
548 | | |
549 | | #[inline] |
550 | 0 | const fn div_p(x: i32, y: i32) -> i32 { |
551 | 0 | if x >= 0 { |
552 | 0 | if y >= 0 { |
553 | 0 | x / y |
554 | | } else { |
555 | 0 | -(x / -y) |
556 | | } |
557 | 0 | } else if y >= 0 { |
558 | 0 | -((y - 1 - x) / y) |
559 | | } else { |
560 | 0 | (-y - 1 - x) / -y |
561 | | } |
562 | 0 | } |
563 | | |
564 | | #[inline] |
565 | 0 | const fn mod_p(x: i32, y: i32) -> i32 { |
566 | 0 | x - y * div_p(x, y) |
567 | 0 | } |
568 | | |
569 | | /// A collection of functions used to prepare data for compression. |
570 | | mod optimize_bytes { |
571 | | /// Integrate over all differences to the previous value in order to |
572 | | /// reconstruct sample values. |
573 | 0 | pub fn differences_to_samples(buffer: &mut [u8]) { |
574 | | // The naive implementation is very simple: |
575 | | // |
576 | | // for index in 1..buffer.len() { |
577 | | // buffer[index] = (buffer[index - 1] as i32 + buffer[index] as i32 - 128) as |
578 | | // u8; } |
579 | | // |
580 | | // But we process elements in pairs to take advantage of instruction-level |
581 | | // parallelism. When computations within a pair do not depend on each |
582 | | // other, they can be processed in parallel. Since this function is |
583 | | // responsible for a very large chunk of execution time, this tweak |
584 | | // alone improves decoding performance of RLE images by 20%. |
585 | 0 | if let Some(first) = buffer.first() { |
586 | 0 | let mut previous = i16::from(*first); |
587 | 0 | for chunk in &mut buffer[1..].chunks_exact_mut(2) { |
588 | 0 | // no bounds checks here due to indices and chunk size being constant |
589 | 0 | let diff0 = i16::from(chunk[0]); |
590 | 0 | let diff1 = i16::from(chunk[1]); |
591 | 0 | // these two computations do not depend on each other, unlike in the naive |
592 | 0 | // version, so they can be executed by the CPU in parallel via |
593 | 0 | // instruction-level parallelism |
594 | 0 | let sample0 = (previous + diff0 - 128) as u8; |
595 | 0 | let sample1 = (previous + diff0 + diff1 - 128 * 2) as u8; |
596 | 0 | chunk[0] = sample0; |
597 | 0 | chunk[1] = sample1; |
598 | 0 | previous = i16::from(sample1); |
599 | 0 | } |
600 | | // handle the remaining element at the end not processed by the loop over pairs, |
601 | | // if present |
602 | 0 | for elem in &mut buffer[1..].chunks_exact_mut(2).into_remainder().iter_mut() { |
603 | 0 | let sample = (previous + i16::from(*elem) - 128) as u8; |
604 | 0 | *elem = sample; |
605 | 0 | previous = i16::from(sample); |
606 | 0 | } |
607 | 0 | } |
608 | 0 | } |
609 | | |
610 | | /// Derive over all values in order to produce differences to the previous |
611 | | /// value. |
612 | 0 | pub fn samples_to_differences(buffer: &mut [u8]) { |
613 | | // naive version: |
614 | | // for index in (1..buffer.len()).rev() { |
615 | | // buffer[index] = (buffer[index] as i32 - buffer[index - 1] as i32 + 128) |
616 | | // as u8; } |
617 | | // |
618 | | // But we process elements in batches to take advantage of autovectorization. |
619 | | // If the target platform has no vector instructions (e.g. 32-bit ARM without |
620 | | // `-C target-cpu=native`) this will instead take advantage of |
621 | | // instruction-level parallelism. |
622 | 0 | if let Some(first) = buffer.first() { |
623 | 0 | let mut previous = i16::from(*first); |
624 | | // Chunk size is 16 because we process bytes (8 bits), |
625 | | // and 8*16 = 128 bits is the size of a typical SIMD register. |
626 | | // Even WASM has 128-bit SIMD registers. |
627 | 0 | for chunk in &mut buffer[1..].chunks_exact_mut(16) { |
628 | 0 | // no bounds checks here due to indices and chunk size being constant |
629 | 0 | let sample0 = i16::from(chunk[0]); |
630 | 0 | let sample1 = i16::from(chunk[1]); |
631 | 0 | let sample2 = i16::from(chunk[2]); |
632 | 0 | let sample3 = i16::from(chunk[3]); |
633 | 0 | let sample4 = i16::from(chunk[4]); |
634 | 0 | let sample5 = i16::from(chunk[5]); |
635 | 0 | let sample6 = i16::from(chunk[6]); |
636 | 0 | let sample7 = i16::from(chunk[7]); |
637 | 0 | let sample8 = i16::from(chunk[8]); |
638 | 0 | let sample9 = i16::from(chunk[9]); |
639 | 0 | let sample10 = i16::from(chunk[10]); |
640 | 0 | let sample11 = i16::from(chunk[11]); |
641 | 0 | let sample12 = i16::from(chunk[12]); |
642 | 0 | let sample13 = i16::from(chunk[13]); |
643 | 0 | let sample14 = i16::from(chunk[14]); |
644 | 0 | let sample15 = i16::from(chunk[15]); |
645 | 0 | // Unlike in decoding, computations in here are truly independent from each |
646 | 0 | // other, which enables the compiler to vectorize this loop. |
647 | 0 | // Even if the target platform has no vector instructions, |
648 | 0 | // so using more parallelism doesn't imply doing more work, |
649 | 0 | // and we're not really limited in how wide we can go. |
650 | 0 | chunk[0] = (sample0 - previous + 128) as u8; |
651 | 0 | chunk[1] = (sample1 - sample0 + 128) as u8; |
652 | 0 | chunk[2] = (sample2 - sample1 + 128) as u8; |
653 | 0 | chunk[3] = (sample3 - sample2 + 128) as u8; |
654 | 0 | chunk[4] = (sample4 - sample3 + 128) as u8; |
655 | 0 | chunk[5] = (sample5 - sample4 + 128) as u8; |
656 | 0 | chunk[6] = (sample6 - sample5 + 128) as u8; |
657 | 0 | chunk[7] = (sample7 - sample6 + 128) as u8; |
658 | 0 | chunk[8] = (sample8 - sample7 + 128) as u8; |
659 | 0 | chunk[9] = (sample9 - sample8 + 128) as u8; |
660 | 0 | chunk[10] = (sample10 - sample9 + 128) as u8; |
661 | 0 | chunk[11] = (sample11 - sample10 + 128) as u8; |
662 | 0 | chunk[12] = (sample12 - sample11 + 128) as u8; |
663 | 0 | chunk[13] = (sample13 - sample12 + 128) as u8; |
664 | 0 | chunk[14] = (sample14 - sample13 + 128) as u8; |
665 | 0 | chunk[15] = (sample15 - sample14 + 128) as u8; |
666 | 0 | previous = sample15; |
667 | 0 | } |
668 | | // Handle the remaining element at the end not processed by the loop over |
669 | | // batches, if present This is what the iterator-based version of |
670 | | // this function would look like without vectorization |
671 | 0 | for elem in &mut buffer[1..].chunks_exact_mut(16).into_remainder().iter_mut() { |
672 | 0 | let diff = (i16::from(*elem) - previous + 128) as u8; |
673 | 0 | previous = i16::from(*elem); |
674 | 0 | *elem = diff; |
675 | 0 | } |
676 | 0 | } |
677 | 0 | } |
678 | | |
679 | | use std::cell::Cell; |
680 | | thread_local! { |
681 | | // A buffer for reusing between invocations of interleaving and deinterleaving. |
682 | | // Allocating memory is cheap, but zeroing or otherwise initializing it is not. |
683 | | // Doing it hundreds of times (once per block) would be expensive. |
684 | | // This optimization brings down the time spent in interleaving from 15% to 5%. |
685 | | static SCRATCH_SPACE: Cell<Vec<u8>> = const { Cell::new(Vec::new()) }; |
686 | | } |
687 | | |
688 | 0 | fn with_reused_buffer<F>(length: usize, mut func: F) |
689 | 0 | where |
690 | 0 | F: FnMut(&mut [u8]), |
691 | | { |
692 | 0 | SCRATCH_SPACE.with(|scratch_space| { |
693 | | // reuse a buffer if we've already initialized one |
694 | 0 | let mut buffer = scratch_space.take(); |
695 | 0 | if buffer.len() < length { |
696 | 0 | // Efficiently create a zeroed Vec by requesting zeroed memory from the OS. |
697 | 0 | // This is slightly faster than a `memcpy()` plus `memset()` that would happen |
698 | 0 | // otherwise, but is not a big deal either way since it's not a |
699 | 0 | // hot codepath. |
700 | 0 | buffer = vec![0u8; length]; |
701 | 0 | } |
702 | | |
703 | | // call the function |
704 | 0 | func(&mut buffer[..length]); |
705 | | |
706 | | // save the internal buffer for reuse |
707 | 0 | scratch_space.set(buffer); |
708 | 0 | }); Unexecuted instantiation: exr::compression::optimize_bytes::with_reused_buffer::<exr::compression::optimize_bytes::interleave_byte_blocks::{closure#0}>::{closure#0}Unexecuted instantiation: exr::compression::optimize_bytes::with_reused_buffer::<exr::compression::optimize_bytes::separate_bytes_fragments::{closure#0}>::{closure#0} |
709 | 0 | } Unexecuted instantiation: exr::compression::optimize_bytes::with_reused_buffer::<exr::compression::optimize_bytes::interleave_byte_blocks::{closure#0}>Unexecuted instantiation: exr::compression::optimize_bytes::with_reused_buffer::<exr::compression::optimize_bytes::separate_bytes_fragments::{closure#0}> |
710 | | |
711 | | /// Interleave the bytes such that the second half of the array is every |
712 | | /// other byte. |
713 | 0 | pub fn interleave_byte_blocks(separated: &mut [u8]) { |
714 | 0 | with_reused_buffer(separated.len(), |interleaved| { |
715 | | // Split the two halves that we are going to interleave. |
716 | 0 | let (first_half, second_half) = separated.split_at((separated.len() + 1) / 2); |
717 | | // The first half can be 1 byte longer than the second if the length of the |
718 | | // input is odd, but the loop below only processes numbers in pairs. |
719 | | // To handle it, preserve the last element of the first slice, to be handled |
720 | | // after the loop. |
721 | 0 | let first_half_last = first_half.last(); |
722 | | // Truncate the first half to match the lenght of the second one; more |
723 | | // optimizer-friendly |
724 | 0 | let first_half_iter = &first_half[..second_half.len()]; |
725 | | |
726 | | // Main loop that performs the interleaving |
727 | 0 | for ((first, second), interleaved) in |
728 | 0 | first_half_iter.iter().zip(second_half.iter()).zip(interleaved.chunks_exact_mut(2)) |
729 | 0 | { |
730 | 0 | // The length of each chunk is known to be 2 at compile time, |
731 | 0 | // and each index is also a constant. |
732 | 0 | // This allows the compiler to remove the bounds checks. |
733 | 0 | interleaved[0] = *first; |
734 | 0 | interleaved[1] = *second; |
735 | 0 | } |
736 | | |
737 | | // If the length of the slice was odd, restore the last element of the first |
738 | | // half that we saved |
739 | 0 | if interleaved.len() % 2 == 1 { |
740 | 0 | if let Some(value) = first_half_last { |
741 | 0 | // we can unwrap() here because we just checked that the lenght is non-zero: |
742 | 0 | // `% 2 == 1` will fail for zero |
743 | 0 | *interleaved.last_mut().unwrap() = *value; |
744 | 0 | } |
745 | 0 | } |
746 | | |
747 | | // write out the results |
748 | 0 | separated.copy_from_slice(interleaved); |
749 | 0 | }); |
750 | 0 | } |
751 | | |
752 | | /// Separate the bytes such that the second half contains every other byte. |
753 | | /// This performs deinterleaving - the inverse of interleaving. |
754 | 0 | pub fn separate_bytes_fragments(source: &mut [u8]) { |
755 | 0 | with_reused_buffer(source.len(), |separated| { |
756 | | // Split the two halves that we are going to interleave. |
757 | 0 | let (first_half, second_half) = separated.split_at_mut((source.len() + 1) / 2); |
758 | | // The first half can be 1 byte longer than the second if the length of the |
759 | | // input is odd, but the loop below only processes numbers in pairs. |
760 | | // To handle it, preserve the last element of the input, to be handled after the |
761 | | // loop. |
762 | 0 | let last = source.last(); |
763 | 0 | let first_half_iter = &mut first_half[..second_half.len()]; |
764 | | |
765 | | // Main loop that performs the deinterleaving |
766 | 0 | for ((first, second), interleaved) in |
767 | 0 | first_half_iter.iter_mut().zip(second_half.iter_mut()).zip(source.chunks_exact(2)) |
768 | 0 | { |
769 | 0 | // The length of each chunk is known to be 2 at compile time, |
770 | 0 | // and each index is also a constant. |
771 | 0 | // This allows the compiler to remove the bounds checks. |
772 | 0 | *first = interleaved[0]; |
773 | 0 | *second = interleaved[1]; |
774 | 0 | } |
775 | | |
776 | | // If the length of the slice was odd, restore the last element of the input |
777 | | // that we saved |
778 | 0 | if source.len() % 2 == 1 { |
779 | 0 | if let Some(value) = last { |
780 | 0 | // we can unwrap() here because we just checked that the lenght is non-zero: |
781 | 0 | // `% 2 == 1` will fail for zero |
782 | 0 | *first_half.last_mut().unwrap() = *value; |
783 | 0 | } |
784 | 0 | } |
785 | | |
786 | | // write out the results |
787 | 0 | source.copy_from_slice(separated); |
788 | 0 | }); |
789 | 0 | } |
790 | | |
791 | | #[cfg(test)] |
792 | | pub mod test { |
793 | | #[test] |
794 | | fn roundtrip_interleave() { |
795 | | let source = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; |
796 | | let mut modified = source.clone(); |
797 | | |
798 | | super::separate_bytes_fragments(&mut modified); |
799 | | super::interleave_byte_blocks(&mut modified); |
800 | | |
801 | | assert_eq!(source, modified); |
802 | | } |
803 | | |
804 | | #[test] |
805 | | fn roundtrip_derive() { |
806 | | let source = vec![0, 1, 2, 7, 4, 5, 6, 7, 13, 9, 10]; |
807 | | let mut modified = source.clone(); |
808 | | |
809 | | super::samples_to_differences(&mut modified); |
810 | | super::differences_to_samples(&mut modified); |
811 | | |
812 | | assert_eq!(source, modified); |
813 | | } |
814 | | } |
815 | | } |
816 | | |
817 | | #[cfg(test)] |
818 | | mod test { |
819 | | use super::*; |
820 | | use crate::{block::samples::IntoNativeSample, meta::attribute::ChannelDescription}; |
821 | | |
822 | | #[test] |
823 | | fn roundtrip_endianness_mixed_channels() { |
824 | | let a32 = ChannelDescription::new("A", SampleType::F32, true); |
825 | | let y16 = ChannelDescription::new("Y", SampleType::F16, true); |
826 | | let channels = ChannelList::new(smallvec![a32, y16]); |
827 | | |
828 | | let data = vec![ |
829 | | (23582740683_f32).to_ne_bytes().as_slice(), |
830 | | (35827420683_f32).to_ne_bytes().as_slice(), |
831 | | (27406832358_f32).to_f16().to_ne_bytes().as_slice(), |
832 | | (74062358283_f32).to_f16().to_ne_bytes().as_slice(), |
833 | | (52582740683_f32).to_ne_bytes().as_slice(), |
834 | | (45827420683_f32).to_ne_bytes().as_slice(), |
835 | | (15406832358_f32).to_f16().to_ne_bytes().as_slice(), |
836 | | (65062358283_f32).to_f16().to_ne_bytes().as_slice(), |
837 | | ] |
838 | | .into_iter() |
839 | | .flatten() |
840 | | .copied() |
841 | | .collect(); |
842 | | |
843 | | roundtrip_convert_endianness(data, &channels, IntegerBounds::from_dimensions((2, 2))); |
844 | | } |
845 | | |
846 | | fn roundtrip_convert_endianness( |
847 | | current_endian: ByteVec, |
848 | | channels: &ChannelList, |
849 | | rectangle: IntegerBounds, |
850 | | ) { |
851 | | let little_endian = |
852 | | convert_current_to_little_endian(current_endian.clone(), channels, rectangle).unwrap(); |
853 | | |
854 | | let current_endian_decoded = |
855 | | convert_little_endian_to_current(little_endian, channels, rectangle).unwrap(); |
856 | | |
857 | | assert_eq!(current_endian, current_endian_decoded, "endianness conversion failed"); |
858 | | } |
859 | | } |