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}