Skip to main content

exr/block/
mod.rs

1//! This is the low-level interface for the raw blocks of an image.
2//! See `exr::image` module for a high-level interface.
3//!
4//! Handle compressed and uncompressed pixel byte blocks. Includes compression
5//! and decompression, and reading a complete image into blocks.
6//!
7//! Start with the `block::read(...)`
8//! and `block::write(...)` functions.
9
10pub mod reader;
11pub mod writer;
12
13pub mod chunk;
14pub mod lines;
15pub mod samples;
16
17use std::io::{Read, Seek, Write};
18
19use crate::{
20    block::{
21        chunk::{
22            Chunk, CompressedBlock, CompressedScanLineBlock, CompressedTileBlock, TileCoordinates,
23        },
24        lines::{LineIndex, LineRef, LineRefMut, LineSlice},
25    },
26    compression::ByteVec,
27    error::{usize_to_i32, Error, Result, UnitResult},
28    math::Vec2,
29    meta::{attribute::ChannelList, header::Header, BlockDescription, Headers, MetaData},
30};
31
32/// Specifies where a block of pixel data should be placed in the actual image.
33/// This is a globally unique identifier which
34/// includes the layer, level index, and pixel location.
35#[derive(Clone, Copy, Eq, Hash, PartialEq, Debug)]
36pub struct BlockIndex {
37    /// Index of the layer.
38    pub layer: usize,
39
40    /// Index of the top left pixel from the block within the data window.
41    pub pixel_position: Vec2<usize>,
42
43    /// Number of pixels in this block, extending to the right and downwards.
44    /// Stays the same across all resolution levels.
45    pub pixel_size: Vec2<usize>,
46
47    /// Index of the mip or rip level in the image.
48    pub level: Vec2<usize>,
49}
50
51/// Contains a block of pixel data and where that data should be placed in the
52/// actual image.
53///
54/// The bytes must be encoded in native-endian format.
55/// The conversion to little-endian format happens when converting to chunks
56/// (potentially in parallel).
57#[derive(Clone, Eq, PartialEq, Debug)]
58pub struct UncompressedBlock {
59    /// Location of the data inside the image.
60    pub index: BlockIndex,
61
62    /// Uncompressed pixel values of the whole block.
63    /// One or more scan lines may be stored together as a scan line block.
64    /// This byte vector contains all pixel rows, one after another.
65    /// For each line in the tile, for each channel, the row values are
66    /// contiguous. Stores all samples of the first channel, then all
67    /// samples of the second channel, and so on. This data is in
68    /// native-endian format.
69    pub data: ByteVec,
70}
71
72/// Immediately reads the meta data from the file.
73///
74/// Then, returns a reader that can be used to read all pixel blocks.
75/// From the reader, you can pull each compressed chunk from the file.
76/// Alternatively, you can create a decompressor, and pull the uncompressed data
77/// from it. The reader is assumed to be buffered.
78pub fn read<R: Read + Seek>(buffered_read: R, pedantic: bool) -> Result<self::reader::Reader<R>> {
79    self::reader::Reader::read_from_buffered(buffered_read, pedantic)
80}
81
82/// Immediately writes the meta data to the file.
83///
84/// Then, calls a closure with a writer that can be used to write all pixel
85/// blocks. In the closure, you can push compressed chunks directly into the
86/// writer. Alternatively, you can create a compressor, wrapping the writer, and
87/// push the uncompressed data to it. The writer is assumed to be buffered.
88pub fn write<W: Write + Seek>(
89    buffered_write: W,
90    headers: Headers,
91    compatibility_checks: bool,
92    write_chunks: impl FnOnce(MetaData, &mut self::writer::ChunkWriter<W>) -> UnitResult,
93) -> UnitResult {
94    self::writer::write_chunks_with(buffered_write, headers, compatibility_checks, write_chunks)
95}
96
97/// This iterator tells you the block indices of all blocks that must be in the
98/// image.
99///
100/// The order of the blocks depends on the `LineOrder` attribute
101/// (unspecified line order is treated the same as increasing line order).
102/// The blocks written to the file must be exactly in this order,
103/// except for when the `LineOrder` is unspecified.
104/// The index represents the block index, in increasing line order, within the
105/// header.
106pub fn enumerate_ordered_header_block_indices(
107    headers: &[Header],
108) -> impl '_ + Iterator<Item = (usize, BlockIndex)> {
109    headers.iter().enumerate().flat_map(|(layer_index, header)| {
110        header.enumerate_ordered_blocks().map(move |(index_in_header, tile)| {
111            let data_indices = header
112                .get_absolute_block_pixel_coordinates(tile.location)
113                .expect("tile coordinate bug");
114
115            let block = BlockIndex {
116                layer: layer_index,
117                level: tile.location.level_index,
118                pixel_position: data_indices
119                    .position
120                    .to_usize("data indices start")
121                    .expect("data index bug"),
122                pixel_size: data_indices.size,
123            };
124
125            (index_in_header, block)
126        })
127    })
128}
129
130impl UncompressedBlock {
131    /// Decompress the possibly compressed chunk and returns an
132    /// `UncompressedBlock`.
133    // for uncompressed data, the ByteVec in the chunk is moved all the way
134    #[inline]
135    #[must_use]
136    pub fn decompress_chunk(chunk: Chunk, meta_data: &MetaData, pedantic: bool) -> Result<Self> {
137        let header: &Header = meta_data
138            .headers
139            .get(chunk.layer_index)
140            .ok_or_else(|| Error::invalid("chunk layer index"))?;
141
142        let tile_data_indices = header.get_block_data_indices(&chunk.compressed_block)?;
143        let absolute_indices = header.get_absolute_block_pixel_coordinates(tile_data_indices)?;
144
145        absolute_indices.validate(Some(header.layer_size))?;
146
147        match chunk.compressed_block {
148            CompressedBlock::Tile(CompressedTileBlock {
149                compressed_pixels_le,
150                ..
151            })
152            | CompressedBlock::ScanLine(CompressedScanLineBlock {
153                compressed_pixels_le,
154                ..
155            }) => Ok(Self {
156                data: header.compression.decompress_image_section_from_le(
157                    header,
158                    compressed_pixels_le,
159                    absolute_indices,
160                    pedantic,
161                )?,
162                index: BlockIndex {
163                    layer: chunk.layer_index,
164                    pixel_position: absolute_indices.position.to_usize("data indices start")?,
165                    level: tile_data_indices.level_index,
166                    pixel_size: absolute_indices.size,
167                },
168            }),
169
170            _ => Err(Error::unsupported("deep data not supported yet")),
171        }
172    }
173
174    /// Consume this block by compressing it, returning a `Chunk`.
175    // for uncompressed data, the ByteVec in the chunk is moved all the way
176    #[inline]
177    #[must_use]
178    pub fn compress_to_chunk(self, headers: &[Header]) -> Result<Chunk> {
179        let Self {
180            data,
181            index,
182        } = self;
183
184        let header: &Header = headers.get(index.layer).expect("block layer index bug");
185
186        let expected_byte_size = header.channels.bytes_per_pixel * self.index.pixel_size.area(); // TODO sampling??
187        if expected_byte_size != data.len() {
188            return Err(Error::invalid(format!(
189                "decompressed block byte size mismatch: expected {} bytes but got {} bytes",
190                expected_byte_size,
191                data.len()
192            )));
193        }
194
195        let tile_coordinates = TileCoordinates {
196            // FIXME this calculation should not be made here but elsewhere instead (in
197            // meta::header?)
198            tile_index: index.pixel_position / header.max_block_pixel_size(), // TODO sampling??
199            level_index: index.level,
200        };
201
202        let absolute_indices = header.get_absolute_block_pixel_coordinates(tile_coordinates)?;
203        absolute_indices.validate(Some(header.layer_size))?;
204
205        if !header.compression.may_loose_data() {
206            debug_assert_eq!(
207                &header
208                    .compression
209                    .decompress_image_section_from_le(
210                        header,
211                        header.compression.compress_image_section_to_le(
212                            header,
213                            data.clone(),
214                            absolute_indices
215                        )?,
216                        absolute_indices,
217                        true
218                    )
219                    .unwrap(),
220                &data,
221                "compression method not round trippin'"
222            );
223        }
224
225        let compressed_pixels_le =
226            header.compression.compress_image_section_to_le(header, data, absolute_indices)?;
227
228        Ok(Chunk {
229            layer_index: index.layer,
230            compressed_block: match header.blocks {
231                BlockDescription::ScanLines => CompressedBlock::ScanLine(CompressedScanLineBlock {
232                    compressed_pixels_le,
233
234                    // FIXME this calculation should not be made here but elsewhere instead (in
235                    // meta::header?)
236                    y_coordinate: usize_to_i32(index.pixel_position.y(), "pixel index")?
237                        + header.own_attributes.layer_position.y(), // TODO sampling??
238                }),
239
240                BlockDescription::Tiles(_) => CompressedBlock::Tile(CompressedTileBlock {
241                    compressed_pixels_le,
242                    coordinates: tile_coordinates,
243                }),
244            },
245        })
246    }
247
248    /// Iterate all the lines in this block.
249    /// Each line contains the all samples for one of the channels.
250    pub fn lines(&self, channels: &ChannelList) -> impl Iterator<Item = LineRef<'_>> {
251        LineIndex::lines_in_block(self.index, channels).map(move |(bytes, line)| LineSlice {
252            location: line,
253            value: &self.data[bytes],
254        })
255    }
256
257    // TODO pub fn lines_mut<'s>(&'s mut self, header: &Header) -> impl 's +
258    // Iterator<Item=LineRefMut<'s>> { LineIndex::lines_in_block(self.index,
259    // &header.channels) .map(move |(bytes, line)| LineSlice { location: line,
260    // value: &mut self.data[bytes] }) }
261
262    // // TODO make iterator
263    // Call a closure for each line of samples in this uncompressed block.
264    // pub fn for_lines(
265    // &self, header: &Header,
266    // mut accept_line: impl FnMut(LineRef<'_>) -> UnitResult
267    // ) -> UnitResult {
268    // for (bytes, line) in LineIndex::lines_in_block(self.index, &header.channels)
269    // { let line_ref = LineSlice { location: line, value: &self.data[bytes] };
270    // accept_line(line_ref)?;
271    // }
272    //
273    // Ok(())
274    // }
275
276    // TODO from iterator??
277    /// Create an uncompressed block byte vector by requesting one line of
278    /// samples after another.
279    pub fn collect_block_data_from_lines(
280        channels: &ChannelList,
281        block_index: BlockIndex,
282        mut extract_line: impl FnMut(LineRefMut<'_>),
283    ) -> Vec<u8> {
284        let byte_count = block_index.pixel_size.area() * channels.bytes_per_pixel;
285        let mut block_bytes = vec![0_u8; byte_count];
286
287        for (byte_range, line_index) in LineIndex::lines_in_block(block_index, channels) {
288            extract_line(LineRefMut {
289                // TODO subsampling
290                value: &mut block_bytes[byte_range],
291                location: line_index,
292            });
293        }
294
295        block_bytes
296    }
297
298    /// Create an uncompressed block by requesting one line of samples after
299    /// another.
300    pub fn from_lines(
301        channels: &ChannelList,
302        block_index: BlockIndex,
303        extract_line: impl FnMut(LineRefMut<'_>),
304    ) -> Self {
305        Self {
306            index: block_index,
307            data: Self::collect_block_data_from_lines(channels, block_index, extract_line),
308        }
309    }
310}