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