Coverage Report

Created: 2026-08-14 08:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}