mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
Rename Raw-rs to Rawkit (#2088)
* Rename within files * Rename in CI * Rename the folder and file names * Rename raw_rs to rawkit * Add example to README * Add initial documentation * Small API changes and extra documentation * Bump versions and stuff * Readme improvements * Merge proc-macro crates into one * Add README to rawkit-proc-macros * Remove keywords and categories * Add licenses to rawkit-proc-macros --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
109
libraries/rawkit/src/decoder/arw1.rs
Normal file
109
libraries/rawkit/src/decoder/arw1.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::tiff::file::TiffRead;
|
||||
use crate::tiff::tags::SonyDataOffset;
|
||||
use crate::tiff::Ifd;
|
||||
use crate::{RawImage, SubtractBlack, Transform};
|
||||
|
||||
use bitstream_io::{BitRead, BitReader, Endianness, BE};
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
pub fn decode_a100<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
|
||||
let data_offset = ifd.get_value::<SonyDataOffset, _>(file).unwrap();
|
||||
|
||||
let image_width = 3881;
|
||||
let image_height = 2608;
|
||||
|
||||
file.seek_from_start(data_offset).unwrap();
|
||||
let mut image = sony_arw_load_raw(image_width, image_height, &mut BitReader::<_, BE>::new(file)).unwrap();
|
||||
|
||||
let len = image.len();
|
||||
image[len - image_width..].fill(0);
|
||||
|
||||
RawImage {
|
||||
data: image,
|
||||
width: image_width,
|
||||
height: image_height,
|
||||
cfa_pattern: todo!(),
|
||||
#[allow(unreachable_code)]
|
||||
maximum: (1 << 12) - 1,
|
||||
black: SubtractBlack::None,
|
||||
transform: Transform::Horizontal,
|
||||
camera_model: None,
|
||||
camera_white_balance: None,
|
||||
white_balance: None,
|
||||
camera_to_rgb: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_and_huffman_decode_file<R: Read + Seek, E: Endianness>(huff: &[u16], file: &mut BitReader<R, E>) -> u32 {
|
||||
let number_of_bits = huff[0].into();
|
||||
let huffman_table = &huff[1..];
|
||||
|
||||
// `number_of_bits` will be no more than 32, so the result is put into a u32
|
||||
let bits: u32 = file.read(number_of_bits).unwrap();
|
||||
let bits = bits as usize;
|
||||
|
||||
let bits_to_seek_from = huffman_table[bits].to_le_bytes()[1] as i64 - number_of_bits as i64;
|
||||
file.seek_bits(std::io::SeekFrom::Current(bits_to_seek_from)).unwrap();
|
||||
|
||||
huffman_table[bits].to_le_bytes()[0].into()
|
||||
}
|
||||
|
||||
fn read_n_bits_from_file<R: Read + Seek, E: Endianness>(number_of_bits: u32, file: &mut BitReader<R, E>) -> u32 {
|
||||
// `number_of_bits` will be no more than 32, so the result is put into a u32
|
||||
file.read(number_of_bits).unwrap()
|
||||
}
|
||||
|
||||
/// ljpeg is a lossless variant of JPEG which gets used for decoding the embedded (thumbnail) preview images in raw files
|
||||
fn ljpeg_diff<R: Read + Seek, E: Endianness>(huff: &[u16], file: &mut BitReader<R, E>, dng_version: Option<u32>) -> i32 {
|
||||
let length = read_and_huffman_decode_file(huff, file);
|
||||
|
||||
if length == 16 && dng_version.map(|x| x >= 0x1010000).unwrap_or(true) {
|
||||
return -32768;
|
||||
}
|
||||
|
||||
let diff = read_n_bits_from_file(length, file) as i32;
|
||||
|
||||
if length == 0 || (diff & (1 << (length - 1))) == 0 {
|
||||
diff - (1 << length) - 1
|
||||
} else {
|
||||
diff
|
||||
}
|
||||
}
|
||||
|
||||
fn sony_arw_load_raw<R: Read + Seek>(width: usize, height: usize, file: &mut BitReader<R, BE>) -> Option<Vec<u16>> {
|
||||
const TABLE: [u16; 18] = [
|
||||
0x0f11, 0x0f10, 0x0e0f, 0x0d0e, 0x0c0d, 0x0b0c, 0x0a0b, 0x090a, 0x0809, 0x0708, 0x0607, 0x0506, 0x0405, 0x0304, 0x0303, 0x0300, 0x0202, 0x0201,
|
||||
];
|
||||
|
||||
let mut huffman_table = [0_u16; 32770];
|
||||
// The first element is the number of bits to read
|
||||
huffman_table[0] = 15;
|
||||
|
||||
let mut n = 0;
|
||||
for x in TABLE {
|
||||
let first_byte = x >> 8;
|
||||
let repeats = 0x8000 >> first_byte;
|
||||
for _ in 0_u16..repeats {
|
||||
n += 1;
|
||||
huffman_table[n] = x;
|
||||
}
|
||||
}
|
||||
|
||||
let mut sum = 0;
|
||||
let mut image = vec![0_u16; width * height];
|
||||
for column in (0..width).rev() {
|
||||
for row in (0..height).step_by(2).chain((1..height).step_by(2)) {
|
||||
sum += ljpeg_diff(&huffman_table, file, None);
|
||||
|
||||
if (sum >> 12) != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if row < height {
|
||||
image[row * width + column] = sum as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(image)
|
||||
}
|
||||
127
libraries/rawkit/src/decoder/arw2.rs
Normal file
127
libraries/rawkit/src/decoder/arw2.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use crate::tiff::file::{Endian, TiffRead};
|
||||
use crate::tiff::tags::{BitsPerSample, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, SonyToneCurve, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels};
|
||||
use crate::tiff::values::CurveLookupTable;
|
||||
use crate::tiff::{Ifd, TiffError};
|
||||
use crate::{RawImage, SubtractBlack, Transform};
|
||||
|
||||
use rawkit_proc_macros::Tag;
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Tag)]
|
||||
struct Arw2Ifd {
|
||||
image_width: ImageWidth,
|
||||
image_height: ImageLength,
|
||||
bits_per_sample: BitsPerSample,
|
||||
compression: Compression,
|
||||
cfa_pattern: CfaPattern,
|
||||
cfa_pattern_dim: CfaPatternDim,
|
||||
strip_offsets: StripOffsets,
|
||||
strip_byte_counts: StripByteCounts,
|
||||
sony_tone_curve: SonyToneCurve,
|
||||
white_balance_levels: Option<WhiteBalanceRggbLevels>,
|
||||
}
|
||||
|
||||
pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
|
||||
let ifd = ifd.get_value::<Arw2Ifd, _>(file).unwrap();
|
||||
|
||||
assert!(ifd.strip_offsets.len() == ifd.strip_byte_counts.len());
|
||||
assert!(ifd.strip_offsets.len() == 1);
|
||||
assert!(ifd.compression == 32767);
|
||||
|
||||
let image_width: usize = ifd.image_width.try_into().unwrap();
|
||||
let image_height: usize = ifd.image_height.try_into().unwrap();
|
||||
let bits_per_sample: usize = ifd.bits_per_sample.into();
|
||||
assert!(bits_per_sample == 12);
|
||||
|
||||
let [cfa_pattern_width, cfa_pattern_height] = ifd.cfa_pattern_dim;
|
||||
assert!(cfa_pattern_width == 2 && cfa_pattern_height == 2);
|
||||
|
||||
file.seek_from_start(ifd.strip_offsets[0]).unwrap();
|
||||
let mut image = sony_arw2_load_raw(image_width, image_height, ifd.sony_tone_curve, file).unwrap();
|
||||
|
||||
// Converting the bps from 12 to 14 so that ARW 2.3.1 and 2.3.5 have the same 14 bps.
|
||||
image.iter_mut().for_each(|x| *x <<= 2);
|
||||
|
||||
RawImage {
|
||||
data: image,
|
||||
width: image_width,
|
||||
height: image_height,
|
||||
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
|
||||
maximum: (1 << 14) - 1,
|
||||
black: SubtractBlack::CfaGrid([512, 512, 512, 512]), // TODO: Find the correct way to do this
|
||||
transform: Transform::Horizontal,
|
||||
camera_model: None,
|
||||
camera_white_balance: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)),
|
||||
white_balance: None,
|
||||
camera_to_rgb: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_u32(buffer: &[u8], endian: Endian) -> Option<u32> {
|
||||
Some(match endian {
|
||||
Endian::Little => u32::from_le_bytes(buffer.try_into().ok()?),
|
||||
Endian::Big => u32::from_be_bytes(buffer.try_into().ok()?),
|
||||
})
|
||||
}
|
||||
|
||||
fn as_u16(buffer: &[u8], endian: Endian) -> Option<u16> {
|
||||
Some(match endian {
|
||||
Endian::Little => u16::from_le_bytes(buffer.try_into().ok()?),
|
||||
Endian::Big => u16::from_be_bytes(buffer.try_into().ok()?),
|
||||
})
|
||||
}
|
||||
|
||||
fn sony_arw2_load_raw<R: Read + Seek>(width: usize, height: usize, curve: CurveLookupTable, file: &mut TiffRead<R>) -> Option<Vec<u16>> {
|
||||
let mut image = vec![0_u16; height * width];
|
||||
let mut data = vec![0_u8; width + 1];
|
||||
|
||||
for row in 0..height {
|
||||
file.read_exact(&mut data[0..width]).unwrap();
|
||||
|
||||
let mut column = 0;
|
||||
let mut data_index = 0;
|
||||
|
||||
while column < width - 30 {
|
||||
let data_value = as_u32(&data[data_index..][..4], file.endian()).unwrap();
|
||||
let max = (0x7ff & data_value) as u16;
|
||||
let min = (0x7ff & data_value >> 11) as u16;
|
||||
let index_to_set_max = 0x0f & data_value >> 22;
|
||||
let index_to_set_min = 0x0f & data_value >> 26;
|
||||
|
||||
let max_minus_min = max as i32 - min as i32;
|
||||
let shift_by_bits = (0..4).find(|&shift| (0x80 << shift) > max_minus_min).unwrap_or(4);
|
||||
|
||||
let mut pixels = [0_u16; 16];
|
||||
let mut bit = 30;
|
||||
for (i, pixel) in pixels.iter_mut().enumerate() {
|
||||
*pixel = match () {
|
||||
_ if i as u32 == index_to_set_max => max,
|
||||
_ if i as u32 == index_to_set_min => min,
|
||||
_ => {
|
||||
let result = as_u16(&data[(data_index + (bit >> 3))..][..2], file.endian()).unwrap();
|
||||
let result = ((result >> (bit & 7)) & 0x07f) << shift_by_bits;
|
||||
|
||||
bit += 7;
|
||||
|
||||
(result + min).min(0x7ff)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for value in pixels {
|
||||
image[row * width + column] = curve.get((value << 1).into()) >> 2;
|
||||
|
||||
// Skip between interlaced columns
|
||||
column += 2;
|
||||
}
|
||||
|
||||
// Switch to the opposite interlaced columns
|
||||
column -= if column & 1 == 0 { 31 } else { 1 };
|
||||
|
||||
data_index += 16;
|
||||
}
|
||||
}
|
||||
|
||||
Some(image)
|
||||
}
|
||||
3
libraries/rawkit/src/decoder/mod.rs
Normal file
3
libraries/rawkit/src/decoder/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod arw1;
|
||||
pub mod arw2;
|
||||
pub mod uncompressed;
|
||||
67
libraries/rawkit/src/decoder/uncompressed.rs
Normal file
67
libraries/rawkit/src/decoder/uncompressed.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use crate::tiff::file::TiffRead;
|
||||
use crate::tiff::tags::{BitsPerSample, BlackLevel, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, RowsPerStrip, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels};
|
||||
use crate::tiff::{Ifd, TiffError};
|
||||
use crate::{RawImage, SubtractBlack, Transform};
|
||||
|
||||
use rawkit_proc_macros::Tag;
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Tag)]
|
||||
struct ArwUncompressedIfd {
|
||||
image_width: ImageWidth,
|
||||
image_height: ImageLength,
|
||||
rows_per_strip: RowsPerStrip,
|
||||
bits_per_sample: BitsPerSample,
|
||||
compression: Compression,
|
||||
black_level: BlackLevel,
|
||||
cfa_pattern: CfaPattern,
|
||||
cfa_pattern_dim: CfaPatternDim,
|
||||
strip_offsets: StripOffsets,
|
||||
strip_byte_counts: StripByteCounts,
|
||||
white_balance_levels: Option<WhiteBalanceRggbLevels>,
|
||||
}
|
||||
|
||||
pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
|
||||
let ifd = ifd.get_value::<ArwUncompressedIfd, _>(file).unwrap();
|
||||
|
||||
assert!(ifd.strip_offsets.len() == ifd.strip_byte_counts.len());
|
||||
assert!(ifd.strip_offsets.len() == 1);
|
||||
assert!(ifd.compression == 1); // 1 is the value for uncompressed format
|
||||
|
||||
let image_width: usize = ifd.image_width.try_into().unwrap();
|
||||
let image_height: usize = ifd.image_height.try_into().unwrap();
|
||||
let rows_per_strip: usize = ifd.rows_per_strip.try_into().unwrap();
|
||||
let bits_per_sample: usize = ifd.bits_per_sample.into();
|
||||
let [cfa_pattern_width, cfa_pattern_height] = ifd.cfa_pattern_dim;
|
||||
assert!(cfa_pattern_width == 2 && cfa_pattern_height == 2);
|
||||
|
||||
let mut image: Vec<u16> = Vec::with_capacity(image_height * image_width);
|
||||
|
||||
for i in 0..ifd.strip_offsets.len() {
|
||||
file.seek_from_start(ifd.strip_offsets[i]).unwrap();
|
||||
|
||||
let last = i == ifd.strip_offsets.len();
|
||||
let rows = if last { image_height % rows_per_strip } else { rows_per_strip };
|
||||
|
||||
for _ in 0..rows {
|
||||
for _ in 0..image_width {
|
||||
image.push(file.read_u16().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RawImage {
|
||||
data: image,
|
||||
width: image_width,
|
||||
height: image_height,
|
||||
cfa_pattern: ifd.cfa_pattern.try_into().unwrap(),
|
||||
maximum: if bits_per_sample == 16 { u16::MAX } else { (1 << bits_per_sample) - 1 },
|
||||
black: SubtractBlack::CfaGrid(ifd.black_level),
|
||||
transform: Transform::Horizontal,
|
||||
camera_model: None,
|
||||
camera_white_balance: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)),
|
||||
white_balance: None,
|
||||
camera_to_rgb: None,
|
||||
}
|
||||
}
|
||||
81
libraries/rawkit/src/demosaicing/linear_demosaicing.rs
Normal file
81
libraries/rawkit/src/demosaicing/linear_demosaicing.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use crate::{Pixel, RawImage};
|
||||
|
||||
fn average(data: &[u16], indexes: impl Iterator<Item = i64>) -> u16 {
|
||||
let mut sum = 0;
|
||||
let mut count = 0;
|
||||
for index in indexes {
|
||||
if index >= 0 && (index as usize) < data.len() {
|
||||
sum += data[index as usize] as u32;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
(sum / count) as u16
|
||||
}
|
||||
|
||||
impl RawImage {
|
||||
pub fn linear_demosaic_iter(&self) -> impl Iterator<Item = Pixel> + use<'_> {
|
||||
match self.cfa_pattern {
|
||||
[0, 1, 1, 2] => self.linear_demosaic_rggb_iter(),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn linear_demosaic_rggb_iter(&self) -> impl Iterator<Item = Pixel> + use<'_> {
|
||||
let width = self.width as i64;
|
||||
let height = self.height as i64;
|
||||
|
||||
(0..height).flat_map(move |row| {
|
||||
let row_by_width = row * width;
|
||||
|
||||
(0..width).map(move |column| {
|
||||
let pixel_index = row_by_width + column;
|
||||
|
||||
let vertical_indexes = [pixel_index + width, pixel_index - width];
|
||||
let horizontal_indexes = [pixel_index + 1, pixel_index - 1];
|
||||
let cross_indexes = [pixel_index + width, pixel_index - width, pixel_index + 1, pixel_index - 1];
|
||||
let diagonal_indexes = [pixel_index + width + 1, pixel_index - width + 1, pixel_index + width - 1, pixel_index - width - 1];
|
||||
|
||||
let pixel_index = pixel_index as usize;
|
||||
match (row % 2 == 0, column % 2 == 0) {
|
||||
(true, true) => Pixel {
|
||||
values: [
|
||||
self.data[pixel_index],
|
||||
average(&self.data, cross_indexes.into_iter()),
|
||||
average(&self.data, diagonal_indexes.into_iter()),
|
||||
],
|
||||
row: row as usize,
|
||||
column: column as usize,
|
||||
},
|
||||
(true, false) => Pixel {
|
||||
values: [
|
||||
average(&self.data, horizontal_indexes.into_iter()),
|
||||
self.data[pixel_index],
|
||||
average(&self.data, vertical_indexes.into_iter()),
|
||||
],
|
||||
row: row as usize,
|
||||
column: column as usize,
|
||||
},
|
||||
(false, true) => Pixel {
|
||||
values: [
|
||||
average(&self.data, vertical_indexes.into_iter()),
|
||||
self.data[pixel_index],
|
||||
average(&self.data, horizontal_indexes.into_iter()),
|
||||
],
|
||||
row: row as usize,
|
||||
column: column as usize,
|
||||
},
|
||||
(false, false) => Pixel {
|
||||
values: [
|
||||
average(&self.data, diagonal_indexes.into_iter()),
|
||||
average(&self.data, cross_indexes.into_iter()),
|
||||
self.data[pixel_index],
|
||||
],
|
||||
row: row as usize,
|
||||
column: column as usize,
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
1
libraries/rawkit/src/demosaicing/mod.rs
Normal file
1
libraries/rawkit/src/demosaicing/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod linear_demosaicing;
|
||||
264
libraries/rawkit/src/lib.rs
Normal file
264
libraries/rawkit/src/lib.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
pub mod decoder;
|
||||
pub mod demosaicing;
|
||||
pub mod metadata;
|
||||
pub mod postprocessing;
|
||||
pub mod preprocessing;
|
||||
pub mod processing;
|
||||
pub mod tiff;
|
||||
|
||||
use crate::metadata::identify::CameraModel;
|
||||
|
||||
use processing::{Pixel, PixelTransform, RawPixel, RawPixelTransform};
|
||||
use rawkit_proc_macros::Tag;
|
||||
use tiff::file::TiffRead;
|
||||
use tiff::tags::{Compression, ImageLength, ImageWidth, Orientation, StripByteCounts, SubIfd, Tag};
|
||||
use tiff::values::Transform;
|
||||
use tiff::{Ifd, TiffError};
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) const CHANNELS_IN_RGB: usize = 3;
|
||||
pub(crate) type Histogram = [[usize; 0x2000]; CHANNELS_IN_RGB];
|
||||
|
||||
/// The amount of black level to be subtracted from Raw Image.
|
||||
pub enum SubtractBlack {
|
||||
/// Don't subtract any value.
|
||||
None,
|
||||
|
||||
/// Subtract a singular value for all pixels in Bayer CFA Grid.
|
||||
Value(u16),
|
||||
|
||||
/// Subtract the appropriate value for pixels in Bayer CFA Grid.
|
||||
CfaGrid([u16; 4]),
|
||||
}
|
||||
|
||||
/// Represents a Raw Image along with its metadata.
|
||||
pub struct RawImage {
|
||||
/// Raw pixel data stored in linear fashion.
|
||||
pub data: Vec<u16>,
|
||||
|
||||
/// Width of the raw image.
|
||||
pub width: usize,
|
||||
|
||||
/// Height of the raw image.
|
||||
pub height: usize,
|
||||
|
||||
/// Bayer CFA pattern used to arrange pixels in [`RawImage::data`].
|
||||
///
|
||||
/// It encodes Red, Blue and Green as 0, 1, and 2 respectively.
|
||||
pub cfa_pattern: [u8; 4],
|
||||
|
||||
/// Transformation to be applied to negate the orientation of camera.
|
||||
pub transform: Transform,
|
||||
|
||||
/// The maximum possible value of pixel that the camera sensor could give.
|
||||
pub maximum: u16,
|
||||
|
||||
/// The minimum possible value of pixel that the camera sensor could give.
|
||||
///
|
||||
/// Used to subtract the black level from the raw image.
|
||||
pub black: SubtractBlack,
|
||||
|
||||
/// Information regarding the company and model of the camera.
|
||||
pub camera_model: Option<CameraModel>,
|
||||
|
||||
/// White balance specified in the metadata of the raw file.
|
||||
///
|
||||
/// It represents the 4 values of CFA Grid which follows the same pattern as [`RawImage::cfa_pattern`].
|
||||
pub camera_white_balance: Option<[f64; 4]>,
|
||||
|
||||
/// White balance of the raw image.
|
||||
///
|
||||
/// It is the same as [`RawImage::camera_white_balance`] if the raw file contains the metadata.
|
||||
/// Otherwise it falls back to calculating the white balance from the color space conversion matrix.
|
||||
///
|
||||
/// It represents the 4 values of CFA Grid which follows the same pattern as [`RawImage::cfa_pattern`].
|
||||
pub white_balance: Option<[f64; 4]>,
|
||||
|
||||
/// Color space conversion matrix to convert from camera's color space to sRGB.
|
||||
pub camera_to_rgb: Option<[[f64; 3]; 3]>,
|
||||
}
|
||||
|
||||
/// Represents the final RGB Image.
|
||||
pub struct Image<T> {
|
||||
/// Pixel data stored in a linear fashion.
|
||||
pub data: Vec<T>,
|
||||
|
||||
/// Width of the image.
|
||||
pub width: usize,
|
||||
|
||||
/// Height of the image.
|
||||
pub height: usize,
|
||||
|
||||
/// The number of color channels in the image.
|
||||
///
|
||||
/// We can assume this will be 3 for all non-obscure, modern cameras.
|
||||
/// See <https://github.com/GraphiteEditor/Graphite/pull/1923#discussion_r1725070342> for more information.
|
||||
pub channels: u8,
|
||||
|
||||
/// The transformation required to orient the image correctly.
|
||||
///
|
||||
/// This will be [`Transform::Horizontal`] after the transform step is applied.
|
||||
pub transform: Transform,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Tag)]
|
||||
struct ArwIfd {
|
||||
image_width: ImageWidth,
|
||||
image_height: ImageLength,
|
||||
compression: Compression,
|
||||
strip_byte_counts: StripByteCounts,
|
||||
}
|
||||
|
||||
impl RawImage {
|
||||
/// Create a [`RawImage`] from an input stream.
|
||||
///
|
||||
/// Decodes the contents of `reader` and extracts raw pixel data and metadata.
|
||||
pub fn decode<R: Read + Seek>(reader: &mut R) -> Result<RawImage, DecoderError> {
|
||||
let mut file = TiffRead::new(reader)?;
|
||||
let ifd = Ifd::new_first_ifd(&mut file)?;
|
||||
|
||||
let camera_model = metadata::identify::identify_camera_model(&ifd, &mut file).unwrap();
|
||||
let transform = ifd.get_value::<Orientation, _>(&mut file)?;
|
||||
|
||||
let mut raw_image = if camera_model.model == "DSLR-A100" {
|
||||
decoder::arw1::decode_a100(ifd, &mut file)
|
||||
} else {
|
||||
let sub_ifd = ifd.get_value::<SubIfd, _>(&mut file)?;
|
||||
let arw_ifd = sub_ifd.get_value::<ArwIfd, _>(&mut file)?;
|
||||
|
||||
if arw_ifd.compression == 1 {
|
||||
decoder::uncompressed::decode(sub_ifd, &mut file)
|
||||
} else if arw_ifd.strip_byte_counts[0] == arw_ifd.image_width * arw_ifd.image_height {
|
||||
decoder::arw2::decode(sub_ifd, &mut file)
|
||||
} else {
|
||||
// TODO: implement for arw 1.
|
||||
todo!()
|
||||
}
|
||||
};
|
||||
|
||||
raw_image.camera_model = Some(camera_model);
|
||||
raw_image.transform = transform;
|
||||
|
||||
raw_image.calculate_conversion_matrices();
|
||||
|
||||
Ok(raw_image)
|
||||
}
|
||||
|
||||
/// Converts the [`RawImage`] to an [`Image`] with 8 bit resolution for each channel.
|
||||
///
|
||||
/// Applies all the processing steps to finally get RGB pixel data.
|
||||
pub fn process_8bit(self) -> Image<u8> {
|
||||
let image = self.process_16bit();
|
||||
|
||||
Image {
|
||||
channels: image.channels,
|
||||
data: image.data.iter().map(|x| (x >> 8) as u8).collect(),
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
transform: image.transform,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the [`RawImage`] to an [`Image`] with 16 bit resolution for each channel.
|
||||
///
|
||||
/// Applies all the processing steps to finally get RGB pixel data.
|
||||
pub fn process_16bit(self) -> Image<u16> {
|
||||
let subtract_black = self.subtract_black_fn();
|
||||
let scale_white_balance = self.scale_white_balance_fn();
|
||||
let scale_to_16bit = self.scale_to_16bit_fn();
|
||||
let raw_image = self.apply((subtract_black, scale_white_balance, scale_to_16bit));
|
||||
|
||||
let convert_to_rgb = raw_image.convert_to_rgb_fn();
|
||||
let mut record_histogram = raw_image.record_histogram_fn();
|
||||
let image = raw_image.demosaic_and_apply((convert_to_rgb, &mut record_histogram));
|
||||
|
||||
let gamma_correction = image.gamma_correction_fn(&record_histogram.histogram);
|
||||
if image.transform == Transform::Horizontal {
|
||||
image.apply(gamma_correction)
|
||||
} else {
|
||||
image.transform_and_apply(gamma_correction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawImage {
|
||||
pub fn apply(mut self, mut transform: impl RawPixelTransform) -> RawImage {
|
||||
for (index, value) in self.data.iter_mut().enumerate() {
|
||||
let pixel = RawPixel {
|
||||
value: *value,
|
||||
row: index / self.width,
|
||||
column: index % self.width,
|
||||
};
|
||||
*value = transform.apply(pixel);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn demosaic_and_apply(self, mut transform: impl PixelTransform) -> Image<u16> {
|
||||
let mut image = vec![0; self.width * self.height * 3];
|
||||
for Pixel { values, row, column } in self.linear_demosaic_iter().map(|mut pixel| {
|
||||
pixel.values = transform.apply(pixel);
|
||||
pixel
|
||||
}) {
|
||||
let pixel_index = row * self.width + column;
|
||||
image[3 * pixel_index..3 * (pixel_index + 1)].copy_from_slice(&values);
|
||||
}
|
||||
|
||||
Image {
|
||||
channels: 3,
|
||||
data: image,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
transform: self.transform,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Image<u16> {
|
||||
pub fn apply(mut self, mut transform: impl PixelTransform) -> Image<u16> {
|
||||
for (index, values) in self.data.chunks_exact_mut(3).enumerate() {
|
||||
let pixel = Pixel {
|
||||
values: values.try_into().unwrap(),
|
||||
row: index / self.width,
|
||||
column: index % self.width,
|
||||
};
|
||||
values.copy_from_slice(&transform.apply(pixel));
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn transform_and_apply(self, mut transform: impl PixelTransform) -> Image<u16> {
|
||||
let mut image = vec![0; self.width * self.height * 3];
|
||||
let (width, height, iter) = self.transform_iter();
|
||||
for Pixel { values, row, column } in iter.map(|mut pixel| {
|
||||
pixel.values = transform.apply(pixel);
|
||||
pixel
|
||||
}) {
|
||||
let pixel_index = row * width + column;
|
||||
image[3 * pixel_index..3 * (pixel_index + 1)].copy_from_slice(&values);
|
||||
}
|
||||
|
||||
Image {
|
||||
channels: 3,
|
||||
data: image,
|
||||
width,
|
||||
height,
|
||||
transform: Transform::Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DecoderError {
|
||||
#[error("An error occurred when trying to parse the TIFF format")]
|
||||
TiffError(#[from] TiffError),
|
||||
#[error("An error occurred when converting integer from one type to another")]
|
||||
ConversionError(#[from] std::num::TryFromIntError),
|
||||
#[error("An IO Error ocurred")]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
||||
118
libraries/rawkit/src/metadata/camera_data.rs
Normal file
118
libraries/rawkit/src/metadata/camera_data.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use crate::RawImage;
|
||||
use rawkit_proc_macros::build_camera_data;
|
||||
|
||||
pub struct CameraData {
|
||||
pub black: u16,
|
||||
pub maximum: u16,
|
||||
pub xyz_to_camera: [i16; 9],
|
||||
}
|
||||
|
||||
impl CameraData {
|
||||
const DEFAULT: CameraData = CameraData {
|
||||
black: 0,
|
||||
maximum: 0,
|
||||
xyz_to_camera: [0; 9],
|
||||
};
|
||||
}
|
||||
|
||||
const CAMERA_DATA: [(&str, CameraData); 40] = build_camera_data!();
|
||||
|
||||
const RGB_TO_XYZ: [[f64; 3]; 3] = [
|
||||
// Matrix:
|
||||
[0.412453, 0.357580, 0.180423],
|
||||
[0.212671, 0.715160, 0.072169],
|
||||
[0.019334, 0.119193, 0.950227],
|
||||
];
|
||||
|
||||
impl RawImage {
|
||||
pub fn calculate_conversion_matrices(&mut self) {
|
||||
let Some(ref camera_model) = self.camera_model else { return };
|
||||
let camera_name_needle = camera_model.make.to_owned() + " " + &camera_model.model;
|
||||
|
||||
let xyz_to_camera = CAMERA_DATA
|
||||
.iter()
|
||||
.find(|(camera_name_haystack, _)| camera_name_needle == *camera_name_haystack)
|
||||
.map(|(_, data)| data.xyz_to_camera.map(|x| (x as f64) / 10_000.));
|
||||
let Some(xyz_to_camera) = xyz_to_camera else { return };
|
||||
|
||||
let mut rgb_to_camera = [[0.; 3]; 3];
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
for k in 0..3 {
|
||||
rgb_to_camera[i][j] += RGB_TO_XYZ[k][j] * xyz_to_camera[i * 3 + k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let white_balance_multiplier = rgb_to_camera.map(|x| 1. / x.iter().sum::<f64>());
|
||||
for (index, row) in rgb_to_camera.iter_mut().enumerate() {
|
||||
*row = row.map(|x| x * white_balance_multiplier[index]);
|
||||
}
|
||||
let camera_to_rgb = transpose(pseudoinverse(rgb_to_camera));
|
||||
|
||||
let cfa_white_balance_multiplier = if let Some(white_balance) = self.camera_white_balance {
|
||||
white_balance
|
||||
} else {
|
||||
self.cfa_pattern.map(|index| white_balance_multiplier[index as usize])
|
||||
};
|
||||
|
||||
self.white_balance = Some(cfa_white_balance_multiplier);
|
||||
self.camera_to_rgb = Some(camera_to_rgb);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
fn pseudoinverse<const N: usize>(matrix: [[f64; 3]; N]) -> [[f64; 3]; N] {
|
||||
let mut output_matrix = [[0.; 3]; N];
|
||||
let mut work = [[0.; 6]; 3];
|
||||
|
||||
for i in 0..3 {
|
||||
for j in 0..6 {
|
||||
work[i][j] = if j == i + 3 { 1. } else { 0. };
|
||||
}
|
||||
for j in 0..3 {
|
||||
for k in 0..N {
|
||||
work[i][j] += matrix[k][i] * matrix[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..3 {
|
||||
let num = work[i][i];
|
||||
for j in 0..6 {
|
||||
work[i][j] /= num;
|
||||
}
|
||||
for k in 0..3 {
|
||||
if k == i {
|
||||
continue;
|
||||
}
|
||||
let num = work[k][i];
|
||||
for j in 0..6 {
|
||||
work[k][j] -= work[i][j] * num;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..N {
|
||||
for j in 0..3 {
|
||||
output_matrix[i][j] = 0.;
|
||||
for k in 0..3 {
|
||||
output_matrix[i][j] += work[j][k + 3] * matrix[i][k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output_matrix
|
||||
}
|
||||
|
||||
fn transpose<const N: usize>(matrix: [[f64; 3]; N]) -> [[f64; N]; 3] {
|
||||
let mut output_matrix = [[0.; N]; 3];
|
||||
|
||||
for (i, row) in matrix.iter().enumerate() {
|
||||
for (j, &value) in row.iter().enumerate() {
|
||||
output_matrix[j][i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
output_matrix
|
||||
}
|
||||
60
libraries/rawkit/src/metadata/identify.rs
Normal file
60
libraries/rawkit/src/metadata/identify.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use crate::tiff::file::TiffRead;
|
||||
use crate::tiff::tags::{Make, Model, Tag};
|
||||
use crate::tiff::{Ifd, TiffError};
|
||||
|
||||
use rawkit_proc_macros::Tag;
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
const COMPANY_NAMES: [&str; 22] = [
|
||||
"AgfaPhoto",
|
||||
"Canon",
|
||||
"Casio",
|
||||
"Epson",
|
||||
"Fujifilm",
|
||||
"Mamiya",
|
||||
"Minolta",
|
||||
"Motorola",
|
||||
"Kodak",
|
||||
"Konica",
|
||||
"Leica",
|
||||
"Nikon",
|
||||
"Nokia",
|
||||
"Olympus",
|
||||
"Ricoh",
|
||||
"Pentax",
|
||||
"Phase One",
|
||||
"Samsung",
|
||||
"Sigma",
|
||||
"Sinar",
|
||||
"Sony",
|
||||
"YI",
|
||||
];
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Tag)]
|
||||
struct CameraModelIfd {
|
||||
make: Make,
|
||||
model: Model,
|
||||
}
|
||||
|
||||
pub struct CameraModel {
|
||||
pub make: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
pub fn identify_camera_model<R: Read + Seek>(ifd: &Ifd, file: &mut TiffRead<R>) -> Option<CameraModel> {
|
||||
let mut ifd = ifd.get_value::<CameraModelIfd, _>(file).unwrap();
|
||||
|
||||
ifd.make.make_ascii_lowercase();
|
||||
for company_name in COMPANY_NAMES {
|
||||
let lowercase_company_name = company_name.to_ascii_lowercase();
|
||||
if ifd.make.contains(&lowercase_company_name) {
|
||||
return Some(CameraModel {
|
||||
make: company_name.to_string(),
|
||||
model: ifd.model,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
2
libraries/rawkit/src/metadata/mod.rs
Normal file
2
libraries/rawkit/src/metadata/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod camera_data;
|
||||
pub mod identify;
|
||||
13
libraries/rawkit/src/postprocessing/convert_to_rgb.rs
Normal file
13
libraries/rawkit/src/postprocessing/convert_to_rgb.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use crate::{Pixel, RawImage, CHANNELS_IN_RGB};
|
||||
|
||||
impl RawImage {
|
||||
pub fn convert_to_rgb_fn(&self) -> impl Fn(Pixel) -> [u16; CHANNELS_IN_RGB] {
|
||||
let Some(camera_to_rgb) = self.camera_to_rgb else { todo!() };
|
||||
|
||||
move |pixel: Pixel| {
|
||||
std::array::from_fn(|i| i)
|
||||
.map(|i| camera_to_rgb[i].iter().zip(pixel.values.iter()).map(|(&coeff, &value)| coeff * value as f64).sum())
|
||||
.map(|x: f64| (x as u16).clamp(0, u16::MAX))
|
||||
}
|
||||
}
|
||||
}
|
||||
84
libraries/rawkit/src/postprocessing/gamma_correction.rs
Normal file
84
libraries/rawkit/src/postprocessing/gamma_correction.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use crate::{Histogram, Image, Pixel, CHANNELS_IN_RGB};
|
||||
use std::f64::consts::E;
|
||||
|
||||
impl Image<u16> {
|
||||
pub fn gamma_correction_fn(&self, histogram: &Histogram) -> impl Fn(Pixel) -> [u16; CHANNELS_IN_RGB] {
|
||||
let percentage = self.width * self.height;
|
||||
|
||||
let mut white = 0;
|
||||
for channel_histogram in histogram {
|
||||
let mut total = 0;
|
||||
for i in (0x20..0x2000).rev() {
|
||||
total += channel_histogram[i] as u64;
|
||||
|
||||
if total * 100 > percentage as u64 {
|
||||
white = white.max(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let curve = generate_gamma_curve(0.45, 4.5, (white << 3) as f64);
|
||||
|
||||
move |pixel: Pixel| pixel.values.map(|value| curve[value as usize])
|
||||
}
|
||||
}
|
||||
|
||||
/// `max_intensity` must be non-zero.
|
||||
fn generate_gamma_curve(power: f64, threshold: f64, max_intensity: f64) -> Vec<u16> {
|
||||
debug_assert!(max_intensity != 0.);
|
||||
|
||||
let (mut bound_start, mut bound_end) = if threshold >= 1. { (0., 1.) } else { (1., 0.) };
|
||||
|
||||
let mut transition_point = 0.;
|
||||
let mut transition_ratio = 0.;
|
||||
let mut curve_adjustment = 0.;
|
||||
|
||||
if threshold != 0. && (threshold - 1.) * (power - 1.) <= 0. {
|
||||
for _ in 0..48 {
|
||||
transition_point = (bound_start + bound_end) / 2.;
|
||||
|
||||
if power != 0. {
|
||||
let temp_transition_ratio = transition_point / threshold;
|
||||
let exponential_power = temp_transition_ratio.powf(-power);
|
||||
let normalized_exponential_power = (exponential_power - 1.) / power;
|
||||
let comparison_result = normalized_exponential_power - (1. / transition_point);
|
||||
|
||||
let bound_to_update = if comparison_result > -1. { &mut bound_end } else { &mut bound_start };
|
||||
*bound_to_update = transition_point;
|
||||
} else {
|
||||
let adjusted_transition_point = E.powf(1. - 1. / transition_point);
|
||||
let transition_point_ratio = transition_point / adjusted_transition_point;
|
||||
|
||||
let bound_to_update = if transition_point_ratio < threshold { &mut bound_end } else { &mut bound_start };
|
||||
*bound_to_update = transition_point;
|
||||
}
|
||||
}
|
||||
|
||||
transition_ratio = transition_point / threshold;
|
||||
|
||||
if power != 0. {
|
||||
curve_adjustment = transition_point * ((1. / power) - 1.);
|
||||
}
|
||||
}
|
||||
|
||||
let mut curve = vec![0xffff; 0x1_0000];
|
||||
let length = curve.len() as f64;
|
||||
|
||||
for (i, entry) in curve.iter_mut().enumerate() {
|
||||
let ratio = (i as f64) / max_intensity;
|
||||
if ratio < 1. {
|
||||
let altered_ratio = if ratio < transition_ratio {
|
||||
ratio * threshold
|
||||
} else if power != 0. {
|
||||
ratio.powf(power) * (1. + curve_adjustment) - curve_adjustment
|
||||
} else {
|
||||
ratio.ln() * transition_point + 1.
|
||||
};
|
||||
|
||||
*entry = (length * altered_ratio) as u16;
|
||||
}
|
||||
}
|
||||
|
||||
curve
|
||||
}
|
||||
4
libraries/rawkit/src/postprocessing/mod.rs
Normal file
4
libraries/rawkit/src/postprocessing/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod convert_to_rgb;
|
||||
pub mod gamma_correction;
|
||||
pub mod record_histogram;
|
||||
pub mod transform;
|
||||
29
libraries/rawkit/src/postprocessing/record_histogram.rs
Normal file
29
libraries/rawkit/src/postprocessing/record_histogram.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use crate::{Histogram, Pixel, PixelTransform, RawImage, CHANNELS_IN_RGB};
|
||||
|
||||
impl RawImage {
|
||||
pub fn record_histogram_fn(&self) -> RecordHistogram {
|
||||
RecordHistogram::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RecordHistogram {
|
||||
pub histogram: Histogram,
|
||||
}
|
||||
|
||||
impl RecordHistogram {
|
||||
fn new() -> RecordHistogram {
|
||||
RecordHistogram {
|
||||
histogram: [[0; 0x2000]; CHANNELS_IN_RGB],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PixelTransform for &mut RecordHistogram {
|
||||
fn apply(&mut self, pixel: Pixel) -> [u16; CHANNELS_IN_RGB] {
|
||||
self.histogram
|
||||
.iter_mut()
|
||||
.zip(pixel.values.iter())
|
||||
.for_each(|(histogram, &value)| histogram[value as usize >> CHANNELS_IN_RGB] += 1);
|
||||
pixel.values
|
||||
}
|
||||
}
|
||||
70
libraries/rawkit/src/postprocessing/transform.rs
Normal file
70
libraries/rawkit/src/postprocessing/transform.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use crate::{Image, Pixel, Transform};
|
||||
|
||||
impl Image<u16> {
|
||||
pub fn transform_iter(&self) -> (usize, usize, impl Iterator<Item = Pixel> + use<'_>) {
|
||||
let (final_width, final_height) = if self.transform.will_swap_coordinates() {
|
||||
(self.height, self.width)
|
||||
} else {
|
||||
(self.width, self.height)
|
||||
};
|
||||
|
||||
let index_0_0 = inverse_transform_index(self.transform, 0, 0, self.width, self.height);
|
||||
let index_0_1 = inverse_transform_index(self.transform, 0, 1, self.width, self.height);
|
||||
let index_1_0 = inverse_transform_index(self.transform, 1, 0, self.width, self.height);
|
||||
|
||||
let column_step = (index_0_1.0 - index_0_0.0, index_0_1.1 - index_0_0.1);
|
||||
let row_step = (index_1_0.0 - index_0_0.0, index_1_0.1 - index_0_0.1);
|
||||
let mut index = index_0_0;
|
||||
|
||||
let channels = self.channels as usize;
|
||||
|
||||
(
|
||||
final_width,
|
||||
final_height,
|
||||
(0..final_height).flat_map(move |row| {
|
||||
let temp = (0..final_width).map(move |column| {
|
||||
let initial_index = (self.width as i64 * index.0 + index.1) as usize;
|
||||
let pixel = &self.data[channels * initial_index..channels * (initial_index + 1)];
|
||||
index = (index.0 + column_step.0, index.1 + column_step.1);
|
||||
|
||||
Pixel {
|
||||
values: pixel.try_into().unwrap(),
|
||||
row,
|
||||
column,
|
||||
}
|
||||
});
|
||||
|
||||
index = (index.0 + row_step.0, index.1 + row_step.1);
|
||||
|
||||
temp
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inverse_transform_index(transform: Transform, mut row: usize, mut column: usize, width: usize, height: usize) -> (i64, i64) {
|
||||
let value = match transform {
|
||||
Transform::Horizontal => 0,
|
||||
Transform::MirrorHorizontal => 1,
|
||||
Transform::Rotate180 => 3,
|
||||
Transform::MirrorVertical => 2,
|
||||
Transform::MirrorHorizontalRotate270 => 4,
|
||||
Transform::Rotate90 => 6,
|
||||
Transform::MirrorHorizontalRotate90 => 7,
|
||||
Transform::Rotate270 => 5,
|
||||
};
|
||||
|
||||
if value & 4 != 0 {
|
||||
std::mem::swap(&mut row, &mut column)
|
||||
}
|
||||
|
||||
if value & 2 != 0 {
|
||||
row = height - 1 - row;
|
||||
}
|
||||
|
||||
if value & 1 != 0 {
|
||||
column = width - 1 - column;
|
||||
}
|
||||
|
||||
(row as i64, column as i64)
|
||||
}
|
||||
3
libraries/rawkit/src/preprocessing/mod.rs
Normal file
3
libraries/rawkit/src/preprocessing/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod scale_to_16bit;
|
||||
pub mod scale_white_balance;
|
||||
pub mod subtract_black;
|
||||
15
libraries/rawkit/src/preprocessing/scale_to_16bit.rs
Normal file
15
libraries/rawkit/src/preprocessing/scale_to_16bit.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use crate::{RawImage, RawPixel, SubtractBlack};
|
||||
|
||||
impl RawImage {
|
||||
pub fn scale_to_16bit_fn(&self) -> impl Fn(RawPixel) -> u16 {
|
||||
let black_level = match self.black {
|
||||
SubtractBlack::CfaGrid(x) => x,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let maximum = self.maximum - black_level.iter().max().unwrap();
|
||||
let scale_to_16bit_multiplier = if maximum > 0 { u16::MAX as f64 / maximum as f64 } else { 1. };
|
||||
|
||||
move |pixel: RawPixel| ((pixel.value as f64) * scale_to_16bit_multiplier).min(u16::MAX as f64).max(0.) as u16
|
||||
}
|
||||
}
|
||||
31
libraries/rawkit/src/preprocessing/scale_white_balance.rs
Normal file
31
libraries/rawkit/src/preprocessing/scale_white_balance.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use crate::{RawImage, RawPixel};
|
||||
|
||||
impl RawImage {
|
||||
pub fn scale_white_balance_fn(&self) -> impl Fn(RawPixel) -> u16 {
|
||||
let Some(mut white_balance) = self.white_balance else { todo!() };
|
||||
|
||||
if white_balance[1] == 0. {
|
||||
white_balance[1] = 1.;
|
||||
}
|
||||
|
||||
// TODO: Move this at its correct location when highlights are implemented correctly.
|
||||
let highlight = 0;
|
||||
|
||||
let normalization_factor = if highlight == 0 {
|
||||
white_balance.into_iter().fold(f64::INFINITY, f64::min)
|
||||
} else {
|
||||
white_balance.into_iter().fold(f64::NEG_INFINITY, f64::max)
|
||||
};
|
||||
|
||||
let normalized_white_balance = if normalization_factor > 0.00001 {
|
||||
white_balance.map(|x| x / normalization_factor)
|
||||
} else {
|
||||
[1., 1., 1., 1.]
|
||||
};
|
||||
|
||||
move |pixel: RawPixel| {
|
||||
let cfa_index = 2 * (pixel.row % 2) + (pixel.column % 2);
|
||||
((pixel.value as f64) * normalized_white_balance[cfa_index]).min(u16::MAX as f64).max(0.) as u16
|
||||
}
|
||||
}
|
||||
}
|
||||
11
libraries/rawkit/src/preprocessing/subtract_black.rs
Normal file
11
libraries/rawkit/src/preprocessing/subtract_black.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use crate::RawPixel;
|
||||
use crate::{RawImage, SubtractBlack};
|
||||
|
||||
impl RawImage {
|
||||
pub fn subtract_black_fn(&self) -> impl Fn(RawPixel) -> u16 {
|
||||
match self.black {
|
||||
SubtractBlack::CfaGrid(black_levels) => move |pixel: RawPixel| pixel.value.saturating_sub(black_levels[2 * (pixel.row % 2) + (pixel.column % 2)]),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
83
libraries/rawkit/src/processing.rs
Normal file
83
libraries/rawkit/src/processing.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use crate::CHANNELS_IN_RGB;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct RawPixel {
|
||||
pub value: u16,
|
||||
pub row: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Pixel {
|
||||
pub values: [u16; CHANNELS_IN_RGB],
|
||||
pub row: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
pub trait RawPixelTransform {
|
||||
fn apply(&mut self, pixel: RawPixel) -> u16;
|
||||
}
|
||||
|
||||
impl<T: Fn(RawPixel) -> u16> RawPixelTransform for T {
|
||||
fn apply(&mut self, pixel: RawPixel) -> u16 {
|
||||
self(pixel)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_raw_pixel_transform {
|
||||
($($idx:tt $t:tt),+) => {
|
||||
impl<$($t,)+> RawPixelTransform for ($($t,)+)
|
||||
where
|
||||
$($t: RawPixelTransform,)+
|
||||
{
|
||||
fn apply(&mut self, mut pixel: RawPixel) -> u16 {
|
||||
$(pixel.value = self.$idx.apply(pixel);)*
|
||||
|
||||
pixel.value
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_raw_pixel_transform!(0 A);
|
||||
impl_raw_pixel_transform!(0 A, 1 B);
|
||||
impl_raw_pixel_transform!(0 A, 1 B, 2 C);
|
||||
impl_raw_pixel_transform!(0 A, 1 B, 2 C, 3 D);
|
||||
impl_raw_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E);
|
||||
impl_raw_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
|
||||
impl_raw_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
|
||||
impl_raw_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
|
||||
|
||||
pub trait PixelTransform {
|
||||
fn apply(&mut self, pixel: Pixel) -> [u16; CHANNELS_IN_RGB];
|
||||
}
|
||||
|
||||
impl<T: Fn(Pixel) -> [u16; CHANNELS_IN_RGB]> PixelTransform for T {
|
||||
fn apply(&mut self, pixel: Pixel) -> [u16; CHANNELS_IN_RGB] {
|
||||
self(pixel)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_pixel_transform {
|
||||
($($idx:tt $t:tt),+) => {
|
||||
impl<$($t,)+> PixelTransform for ($($t,)+)
|
||||
where
|
||||
$($t: PixelTransform,)+
|
||||
{
|
||||
fn apply(&mut self, mut pixel: Pixel) -> [u16; CHANNELS_IN_RGB] {
|
||||
$(pixel.values = self.$idx.apply(pixel);)*
|
||||
|
||||
pixel.values
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_pixel_transform!(0 A);
|
||||
impl_pixel_transform!(0 A, 1 B);
|
||||
impl_pixel_transform!(0 A, 1 B, 2 C);
|
||||
impl_pixel_transform!(0 A, 1 B, 2 C, 3 D);
|
||||
impl_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E);
|
||||
impl_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
|
||||
impl_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
|
||||
impl_pixel_transform!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
|
||||
152
libraries/rawkit/src/tiff/file.rs
Normal file
152
libraries/rawkit/src/tiff/file.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use std::io::{Error, ErrorKind, Read, Result, Seek, SeekFrom};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Endian {
|
||||
Little,
|
||||
Big,
|
||||
}
|
||||
|
||||
pub struct TiffRead<R: Read + Seek> {
|
||||
reader: R,
|
||||
endian: Endian,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> TiffRead<R> {
|
||||
pub fn new(mut reader: R) -> Result<Self> {
|
||||
let error = Error::new(ErrorKind::InvalidData, "Invalid Tiff format");
|
||||
|
||||
let mut data = [0_u8; 2];
|
||||
reader.read_exact(&mut data)?;
|
||||
let endian = if data[0] == 0x49 && data[1] == 0x49 {
|
||||
Endian::Little
|
||||
} else if data[0] == 0x4d && data[1] == 0x4d {
|
||||
Endian::Big
|
||||
} else {
|
||||
return Err(error);
|
||||
};
|
||||
|
||||
reader.read_exact(&mut data)?;
|
||||
let magic_number = match endian {
|
||||
Endian::Little => u16::from_le_bytes(data),
|
||||
Endian::Big => u16::from_be_bytes(data),
|
||||
};
|
||||
if magic_number != 42 {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(Self { reader, endian })
|
||||
}
|
||||
|
||||
pub fn endian(&self) -> Endian {
|
||||
self.endian
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Read for TiffRead<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
|
||||
self.reader.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Seek for TiffRead<R> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
|
||||
self.reader.seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> TiffRead<R> {
|
||||
pub fn seek_from_start(&mut self, offset: u32) -> Result<u64> {
|
||||
self.reader.seek(SeekFrom::Start(offset.into()))
|
||||
}
|
||||
|
||||
pub fn read_ascii(&mut self) -> Result<char> {
|
||||
let data = self.read_n::<1>()?;
|
||||
Ok(data[0] as char)
|
||||
}
|
||||
|
||||
pub fn read_n<const N: usize>(&mut self) -> Result<[u8; N]> {
|
||||
let mut data = [0_u8; N];
|
||||
self.read_exact(&mut data)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub fn read_u8(&mut self) -> Result<u8> {
|
||||
let data = self.read_n()?;
|
||||
match self.endian {
|
||||
Endian::Little => Ok(u8::from_le_bytes(data)),
|
||||
Endian::Big => Ok(u8::from_be_bytes(data)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_u16(&mut self) -> Result<u16> {
|
||||
let data = self.read_n()?;
|
||||
match self.endian {
|
||||
Endian::Little => Ok(u16::from_le_bytes(data)),
|
||||
Endian::Big => Ok(u16::from_be_bytes(data)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_u32(&mut self) -> Result<u32> {
|
||||
let data = self.read_n()?;
|
||||
match self.endian {
|
||||
Endian::Little => Ok(u32::from_le_bytes(data)),
|
||||
Endian::Big => Ok(u32::from_be_bytes(data)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_u64(&mut self) -> Result<u64> {
|
||||
let data = self.read_n()?;
|
||||
match self.endian {
|
||||
Endian::Little => Ok(u64::from_le_bytes(data)),
|
||||
Endian::Big => Ok(u64::from_be_bytes(data)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_i8(&mut self) -> Result<i8> {
|
||||
let data = self.read_n()?;
|
||||
match self.endian {
|
||||
Endian::Little => Ok(i8::from_le_bytes(data)),
|
||||
Endian::Big => Ok(i8::from_be_bytes(data)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_i16(&mut self) -> Result<i16> {
|
||||
let data = self.read_n()?;
|
||||
match self.endian {
|
||||
Endian::Little => Ok(i16::from_le_bytes(data)),
|
||||
Endian::Big => Ok(i16::from_be_bytes(data)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_i32(&mut self) -> Result<i32> {
|
||||
let data = self.read_n()?;
|
||||
match self.endian {
|
||||
Endian::Little => Ok(i32::from_le_bytes(data)),
|
||||
Endian::Big => Ok(i32::from_be_bytes(data)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_i64(&mut self) -> Result<i64> {
|
||||
let data = self.read_n()?;
|
||||
match self.endian {
|
||||
Endian::Little => Ok(i64::from_le_bytes(data)),
|
||||
Endian::Big => Ok(i64::from_be_bytes(data)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_f32(&mut self) -> Result<f32> {
|
||||
let data = self.read_n()?;
|
||||
match self.endian {
|
||||
Endian::Little => Ok(f32::from_le_bytes(data)),
|
||||
Endian::Big => Ok(f32::from_be_bytes(data)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_f64(&mut self) -> Result<f64> {
|
||||
let data = self.read_n()?;
|
||||
match self.endian {
|
||||
Endian::Little => Ok(f64::from_le_bytes(data)),
|
||||
Endian::Big => Ok(f64::from_be_bytes(data)),
|
||||
}
|
||||
}
|
||||
}
|
||||
172
libraries/rawkit/src/tiff/mod.rs
Normal file
172
libraries/rawkit/src/tiff/mod.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
pub mod file;
|
||||
pub mod tags;
|
||||
mod types;
|
||||
pub mod values;
|
||||
|
||||
use file::TiffRead;
|
||||
use tags::Tag;
|
||||
|
||||
use num_enum::{FromPrimitive, IntoPrimitive};
|
||||
use std::fmt::Display;
|
||||
use std::io::{Read, Seek};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
|
||||
#[repr(u16)]
|
||||
pub enum TagId {
|
||||
ImageWidth = 0x100,
|
||||
ImageLength = 0x101,
|
||||
BitsPerSample = 0x102,
|
||||
Compression = 0x103,
|
||||
PhotometricInterpretation = 0x104,
|
||||
Make = 0x10f,
|
||||
Model = 0x110,
|
||||
StripOffsets = 0x111,
|
||||
Orientation = 0x112,
|
||||
SamplesPerPixel = 0x115,
|
||||
RowsPerStrip = 0x116,
|
||||
StripByteCounts = 0x117,
|
||||
SubIfd = 0x14a,
|
||||
JpegOffset = 0x201,
|
||||
JpegLength = 0x202,
|
||||
SonyToneCurve = 0x7010,
|
||||
BlackLevel = 0x7310,
|
||||
WhiteBalanceRggbLevels = 0x7313,
|
||||
CfaPatternDim = 0x828d,
|
||||
CfaPattern = 0x828e,
|
||||
ColorMatrix1 = 0xc621,
|
||||
ColorMatrix2 = 0xc622,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive, IntoPrimitive)]
|
||||
pub enum IfdTagType {
|
||||
Byte = 1,
|
||||
Ascii = 2,
|
||||
Short = 3,
|
||||
Long = 4,
|
||||
Rational = 5,
|
||||
SByte = 6,
|
||||
Undefined = 7,
|
||||
SShort = 8,
|
||||
SLong = 9,
|
||||
SRational = 10,
|
||||
Float = 11,
|
||||
Double = 12,
|
||||
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct IfdEntry {
|
||||
tag: TagId,
|
||||
the_type: IfdTagType,
|
||||
count: u32,
|
||||
value: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Ifd {
|
||||
current_ifd_offset: u32,
|
||||
ifd_entries: Vec<IfdEntry>,
|
||||
next_ifd_offset: Option<u32>,
|
||||
}
|
||||
|
||||
impl Ifd {
|
||||
pub fn new_first_ifd<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self, TiffError> {
|
||||
file.seek_from_start(4)?;
|
||||
let current_ifd_offset = file.read_u32()?;
|
||||
Ifd::new_from_offset(file, current_ifd_offset)
|
||||
}
|
||||
|
||||
pub fn new_from_offset<R: Read + Seek>(file: &mut TiffRead<R>, offset: u32) -> Result<Self, TiffError> {
|
||||
if offset == 0 {
|
||||
return Err(TiffError::InvalidOffset);
|
||||
}
|
||||
|
||||
file.seek_from_start(offset)?;
|
||||
let num = file.read_u16()?;
|
||||
|
||||
let mut ifd_entries = Vec::with_capacity(num.into());
|
||||
for _ in 0..num {
|
||||
let tag = file.read_u16()?.into();
|
||||
let the_type = file.read_u16()?.into();
|
||||
let count = file.read_u32()?;
|
||||
let value = file.read_u32()?;
|
||||
|
||||
ifd_entries.push(IfdEntry { tag, the_type, count, value });
|
||||
}
|
||||
|
||||
let next_ifd_offset = file.read_u32()?;
|
||||
let next_ifd_offset = if next_ifd_offset == 0 { None } else { Some(next_ifd_offset) };
|
||||
|
||||
Ok(Ifd {
|
||||
current_ifd_offset: offset,
|
||||
ifd_entries,
|
||||
next_ifd_offset,
|
||||
})
|
||||
}
|
||||
|
||||
fn _next_ifd<R: Read + Seek>(&self, file: &mut TiffRead<R>) -> Result<Self, TiffError> {
|
||||
Ifd::new_from_offset(file, self.next_ifd_offset.unwrap_or(0))
|
||||
}
|
||||
|
||||
pub fn ifd_entries(&self) -> &[IfdEntry] {
|
||||
&self.ifd_entries
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &IfdEntry> {
|
||||
self.ifd_entries.iter()
|
||||
}
|
||||
|
||||
pub fn get_value<T: Tag, R: Read + Seek>(&self, file: &mut TiffRead<R>) -> Result<T::Output, TiffError> {
|
||||
T::get(self, file)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Ifd {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("IFD offset: ")?;
|
||||
self.current_ifd_offset.fmt(f)?;
|
||||
f.write_str("\n")?;
|
||||
|
||||
for ifd_entry in self.ifd_entries() {
|
||||
f.write_fmt(format_args!(
|
||||
"|- Tag: {:x?}, Type: {:?}, Count: {}, Value: {:x}\n",
|
||||
ifd_entry.tag, ifd_entry.the_type, ifd_entry.count, ifd_entry.value
|
||||
))?;
|
||||
}
|
||||
|
||||
f.write_str("Next IFD offset: ")?;
|
||||
if let Some(offset) = self.next_ifd_offset {
|
||||
offset.fmt(f)?;
|
||||
} else {
|
||||
f.write_str("None")?;
|
||||
}
|
||||
f.write_str("\n")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TiffError {
|
||||
#[error("The value was invalid")]
|
||||
InvalidValue,
|
||||
#[error("The type was invalid")]
|
||||
InvalidType,
|
||||
#[error("The count was invalid")]
|
||||
InvalidCount,
|
||||
#[error("The tag was missing")]
|
||||
MissingTag,
|
||||
#[error("The offset was invalid or zero")]
|
||||
InvalidOffset,
|
||||
#[error("An error occurred when converting integer from one type to another")]
|
||||
ConversionError(#[from] std::num::TryFromIntError),
|
||||
#[error("An IO Error ocurred")]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
||||
228
libraries/rawkit/src/tiff/tags.rs
Normal file
228
libraries/rawkit/src/tiff/tags.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use super::types::{Array, ConstArray, TagType, TypeByte, TypeIfd, TypeLong, TypeNumber, TypeOrientation, TypeSRational, TypeSShort, TypeShort, TypeSonyToneCurve, TypeString};
|
||||
use super::{Ifd, TagId, TiffError, TiffRead};
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
pub trait SimpleTag {
|
||||
type Type: TagType;
|
||||
|
||||
const ID: TagId;
|
||||
const NAME: &'static str;
|
||||
}
|
||||
|
||||
pub struct ImageWidth;
|
||||
pub struct ImageLength;
|
||||
pub struct BitsPerSample;
|
||||
pub struct Compression;
|
||||
pub struct PhotometricInterpretation;
|
||||
pub struct Make;
|
||||
pub struct Model;
|
||||
pub struct StripOffsets;
|
||||
pub struct Orientation;
|
||||
pub struct SamplesPerPixel;
|
||||
pub struct RowsPerStrip;
|
||||
pub struct StripByteCounts;
|
||||
pub struct SubIfd;
|
||||
pub struct JpegOffset;
|
||||
pub struct JpegLength;
|
||||
pub struct SonyDataOffset;
|
||||
pub struct SonyToneCurve;
|
||||
pub struct BlackLevel;
|
||||
pub struct WhiteBalanceRggbLevels;
|
||||
pub struct CfaPatternDim;
|
||||
pub struct CfaPattern;
|
||||
pub struct ColorMatrix1;
|
||||
pub struct ColorMatrix2;
|
||||
|
||||
impl SimpleTag for ImageWidth {
|
||||
type Type = TypeNumber;
|
||||
|
||||
const ID: TagId = TagId::ImageWidth;
|
||||
const NAME: &'static str = "Image Width";
|
||||
}
|
||||
|
||||
impl SimpleTag for ImageLength {
|
||||
type Type = TypeNumber;
|
||||
|
||||
const ID: TagId = TagId::ImageLength;
|
||||
const NAME: &'static str = "Image Length";
|
||||
}
|
||||
|
||||
impl SimpleTag for BitsPerSample {
|
||||
type Type = TypeShort;
|
||||
|
||||
const ID: TagId = TagId::BitsPerSample;
|
||||
const NAME: &'static str = "Bits per Sample";
|
||||
}
|
||||
|
||||
impl SimpleTag for Compression {
|
||||
type Type = TypeShort;
|
||||
|
||||
const ID: TagId = TagId::Compression;
|
||||
const NAME: &'static str = "Compression";
|
||||
}
|
||||
|
||||
impl SimpleTag for PhotometricInterpretation {
|
||||
type Type = TypeShort;
|
||||
|
||||
const ID: TagId = TagId::PhotometricInterpretation;
|
||||
const NAME: &'static str = "Photometric Interpretation";
|
||||
}
|
||||
|
||||
impl SimpleTag for Make {
|
||||
type Type = TypeString;
|
||||
|
||||
const ID: TagId = TagId::Make;
|
||||
const NAME: &'static str = "Make";
|
||||
}
|
||||
|
||||
impl SimpleTag for Model {
|
||||
type Type = TypeString;
|
||||
|
||||
const ID: TagId = TagId::Model;
|
||||
const NAME: &'static str = "Model";
|
||||
}
|
||||
|
||||
impl SimpleTag for StripOffsets {
|
||||
type Type = Array<TypeNumber>;
|
||||
|
||||
const ID: TagId = TagId::StripOffsets;
|
||||
const NAME: &'static str = "Strip Offsets";
|
||||
}
|
||||
|
||||
impl SimpleTag for Orientation {
|
||||
type Type = TypeOrientation;
|
||||
|
||||
const ID: TagId = TagId::Orientation;
|
||||
const NAME: &'static str = "Orientation";
|
||||
}
|
||||
|
||||
impl SimpleTag for SamplesPerPixel {
|
||||
type Type = TypeShort;
|
||||
|
||||
const ID: TagId = TagId::SamplesPerPixel;
|
||||
const NAME: &'static str = "Samples per Pixel";
|
||||
}
|
||||
|
||||
impl SimpleTag for RowsPerStrip {
|
||||
type Type = TypeNumber;
|
||||
|
||||
const ID: TagId = TagId::RowsPerStrip;
|
||||
const NAME: &'static str = "Rows per Strip";
|
||||
}
|
||||
|
||||
impl SimpleTag for StripByteCounts {
|
||||
type Type = Array<TypeNumber>;
|
||||
|
||||
const ID: TagId = TagId::StripByteCounts;
|
||||
const NAME: &'static str = "Strip Byte Counts";
|
||||
}
|
||||
|
||||
impl SimpleTag for SubIfd {
|
||||
type Type = TypeIfd;
|
||||
|
||||
const ID: TagId = TagId::SubIfd;
|
||||
const NAME: &'static str = "SubIFD";
|
||||
}
|
||||
|
||||
impl SimpleTag for JpegOffset {
|
||||
type Type = TypeLong;
|
||||
|
||||
const ID: TagId = TagId::JpegOffset;
|
||||
const NAME: &'static str = "Jpeg Offset";
|
||||
}
|
||||
|
||||
impl SimpleTag for JpegLength {
|
||||
type Type = TypeLong;
|
||||
|
||||
const ID: TagId = TagId::JpegLength;
|
||||
const NAME: &'static str = "Jpeg Length";
|
||||
}
|
||||
|
||||
impl SimpleTag for CfaPatternDim {
|
||||
type Type = ConstArray<TypeShort, 2>;
|
||||
|
||||
const ID: TagId = TagId::CfaPatternDim;
|
||||
const NAME: &'static str = "CFA Pattern Dimension";
|
||||
}
|
||||
|
||||
impl SimpleTag for CfaPattern {
|
||||
type Type = Array<TypeByte>;
|
||||
|
||||
const ID: TagId = TagId::CfaPattern;
|
||||
const NAME: &'static str = "CFA Pattern";
|
||||
}
|
||||
|
||||
impl SimpleTag for ColorMatrix1 {
|
||||
type Type = Array<TypeSRational>;
|
||||
|
||||
const ID: TagId = TagId::ColorMatrix1;
|
||||
const NAME: &'static str = "Color Matrix 1";
|
||||
}
|
||||
|
||||
impl SimpleTag for ColorMatrix2 {
|
||||
type Type = Array<TypeSRational>;
|
||||
|
||||
const ID: TagId = TagId::ColorMatrix2;
|
||||
const NAME: &'static str = "Color Matrix 2";
|
||||
}
|
||||
|
||||
impl SimpleTag for SonyDataOffset {
|
||||
type Type = TypeLong;
|
||||
|
||||
const ID: TagId = TagId::SubIfd;
|
||||
const NAME: &'static str = "Sony Data Offset";
|
||||
}
|
||||
|
||||
impl SimpleTag for SonyToneCurve {
|
||||
type Type = TypeSonyToneCurve;
|
||||
|
||||
const ID: TagId = TagId::SonyToneCurve;
|
||||
const NAME: &'static str = "Sony Tone Curve";
|
||||
}
|
||||
|
||||
impl SimpleTag for BlackLevel {
|
||||
type Type = ConstArray<TypeShort, 4>;
|
||||
|
||||
const ID: TagId = TagId::BlackLevel;
|
||||
const NAME: &'static str = "Black Level";
|
||||
}
|
||||
|
||||
impl SimpleTag for WhiteBalanceRggbLevels {
|
||||
type Type = ConstArray<TypeSShort, 4>;
|
||||
|
||||
const ID: TagId = TagId::WhiteBalanceRggbLevels;
|
||||
const NAME: &'static str = "White Balance Levels (RGGB)";
|
||||
}
|
||||
|
||||
pub trait Tag {
|
||||
type Output;
|
||||
|
||||
fn get<R: Read + Seek>(ifd: &Ifd, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError>;
|
||||
}
|
||||
|
||||
impl<T: SimpleTag> Tag for T {
|
||||
type Output = <T::Type as TagType>::Output;
|
||||
|
||||
fn get<R: Read + Seek>(ifd: &Ifd, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let tag_id = T::ID;
|
||||
let index: u32 = ifd.iter().position(|x| x.tag == tag_id).ok_or(TiffError::MissingTag)?.try_into()?;
|
||||
|
||||
file.seek_from_start(ifd.current_ifd_offset + 2 + 12 * index + 2)?;
|
||||
T::Type::read(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Tag> Tag for Option<T> {
|
||||
type Output = Option<T::Output>;
|
||||
|
||||
fn get<R: Read + Seek>(ifd: &Ifd, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let result = T::get(ifd, file);
|
||||
|
||||
match result {
|
||||
Err(TiffError::MissingTag) => Ok(None),
|
||||
Ok(x) => Ok(Some(x)),
|
||||
Err(x) => Err(x),
|
||||
}
|
||||
}
|
||||
}
|
||||
399
libraries/rawkit/src/tiff/types.rs
Normal file
399
libraries/rawkit/src/tiff/types.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use super::file::TiffRead;
|
||||
use super::values::{CurveLookupTable, Rational, Transform};
|
||||
use super::{Ifd, IfdTagType, TiffError};
|
||||
|
||||
pub struct TypeAscii;
|
||||
pub struct TypeByte;
|
||||
pub struct TypeShort;
|
||||
pub struct TypeLong;
|
||||
pub struct TypeRational;
|
||||
pub struct TypeSByte;
|
||||
pub struct TypeSShort;
|
||||
pub struct TypeSLong;
|
||||
pub struct TypeSRational;
|
||||
pub struct TypeFloat;
|
||||
pub struct TypeDouble;
|
||||
pub struct TypeUndefined;
|
||||
|
||||
pub struct TypeNumber;
|
||||
pub struct TypeSNumber;
|
||||
pub struct TypeIfd;
|
||||
|
||||
pub trait PrimitiveType {
|
||||
type Output;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32>;
|
||||
|
||||
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError>;
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeAscii {
|
||||
type Output = char;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::Ascii => Some(1),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let value = file.read_ascii()?;
|
||||
if value.is_ascii() {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(TiffError::InvalidValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeByte {
|
||||
type Output = u8;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::Byte => Some(1),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(file.read_u8()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeShort {
|
||||
type Output = u16;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::Short => Some(2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(file.read_u16()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeLong {
|
||||
type Output = u32;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::Long => Some(4),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(file.read_u32()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeRational {
|
||||
type Output = Rational<u32>;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::Rational => Some(8),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let numerator = TypeLong::read_primitive(the_type, file)?;
|
||||
let denominator = TypeLong::read_primitive(the_type, file)?;
|
||||
|
||||
Ok(Rational { numerator, denominator })
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeSByte {
|
||||
type Output = i8;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::SByte => Some(1),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(file.read_i8()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeSShort {
|
||||
type Output = i16;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::SShort => Some(2),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(file.read_i16()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeSLong {
|
||||
type Output = i32;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::SLong => Some(4),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(file.read_i32()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeSRational {
|
||||
type Output = Rational<i32>;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::SRational => Some(8),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let numerator = TypeSLong::read_primitive(the_type, file)?;
|
||||
let denominator = TypeSLong::read_primitive(the_type, file)?;
|
||||
|
||||
Ok(Rational { numerator, denominator })
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeFloat {
|
||||
type Output = f32;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::Float => Some(4),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(file.read_f32()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeDouble {
|
||||
type Output = f64;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::Double => Some(8),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(_: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(file.read_f64()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeUndefined {
|
||||
type Output = ();
|
||||
|
||||
fn get_size(_: IfdTagType) -> Option<u32> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(_: IfdTagType, _: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeNumber {
|
||||
type Output = u32;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::Byte => TypeByte::get_size(the_type),
|
||||
IfdTagType::Short => TypeShort::get_size(the_type),
|
||||
IfdTagType::Long => TypeLong::get_size(the_type),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(match the_type {
|
||||
IfdTagType::Byte => TypeByte::read_primitive(the_type, file)?.into(),
|
||||
IfdTagType::Short => TypeShort::read_primitive(the_type, file)?.into(),
|
||||
IfdTagType::Long => TypeLong::read_primitive(the_type, file)?,
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeSNumber {
|
||||
type Output = i32;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
match the_type {
|
||||
IfdTagType::SByte => TypeSByte::get_size(the_type),
|
||||
IfdTagType::SShort => TypeSShort::get_size(the_type),
|
||||
IfdTagType::SLong => TypeSLong::get_size(the_type),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(match the_type {
|
||||
IfdTagType::SByte => TypeSByte::read_primitive(the_type, file)?.into(),
|
||||
IfdTagType::SShort => TypeSShort::read_primitive(the_type, file)?.into(),
|
||||
IfdTagType::SLong => TypeSLong::read_primitive(the_type, file)?,
|
||||
_ => unreachable!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimitiveType for TypeIfd {
|
||||
type Output = Ifd;
|
||||
|
||||
fn get_size(the_type: IfdTagType) -> Option<u32> {
|
||||
TypeLong::get_size(the_type)
|
||||
}
|
||||
|
||||
fn read_primitive<R: Read + Seek>(the_type: IfdTagType, file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let offset = TypeLong::read_primitive(the_type, file)?;
|
||||
Ifd::new_from_offset(file, offset)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TagType {
|
||||
type Output;
|
||||
|
||||
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError>;
|
||||
}
|
||||
|
||||
impl<T: PrimitiveType> TagType for T {
|
||||
type Output = T::Output;
|
||||
|
||||
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let the_type = IfdTagType::from(file.read_u16()?);
|
||||
let count = file.read_u32()?;
|
||||
|
||||
if count != 1 {
|
||||
return Err(TiffError::InvalidCount);
|
||||
}
|
||||
|
||||
let size = T::get_size(the_type).ok_or(TiffError::InvalidType)?;
|
||||
if count * size > 4 {
|
||||
let offset = file.read_u32()?;
|
||||
file.seek_from_start(offset)?;
|
||||
}
|
||||
|
||||
T::read_primitive(the_type, file)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Array<T: PrimitiveType> {
|
||||
primitive_type: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
pub struct ConstArray<T: PrimitiveType, const N: usize> {
|
||||
primitive_type: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: PrimitiveType> TagType for Array<T> {
|
||||
type Output = Vec<T::Output>;
|
||||
|
||||
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let the_type = IfdTagType::from(file.read_u16()?);
|
||||
let count = file.read_u32()?;
|
||||
|
||||
let size = T::get_size(the_type).ok_or(TiffError::InvalidType)?;
|
||||
if count * size > 4 {
|
||||
let offset = file.read_u32()?;
|
||||
file.seek_from_start(offset)?;
|
||||
}
|
||||
|
||||
let mut ans = Vec::with_capacity(count.try_into()?);
|
||||
for _ in 0..count {
|
||||
ans.push(T::read_primitive(the_type, file)?);
|
||||
}
|
||||
Ok(ans)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PrimitiveType, const N: usize> TagType for ConstArray<T, N> {
|
||||
type Output = [T::Output; N];
|
||||
|
||||
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let the_type = IfdTagType::from(file.read_u16()?);
|
||||
let count = file.read_u32()?;
|
||||
|
||||
if count != N.try_into()? {
|
||||
return Err(TiffError::InvalidCount);
|
||||
}
|
||||
|
||||
let size = T::get_size(the_type).ok_or(TiffError::InvalidType)?;
|
||||
if count * size > 4 {
|
||||
let offset = file.read_u32()?;
|
||||
file.seek_from_start(offset)?;
|
||||
}
|
||||
|
||||
let mut ans = Vec::with_capacity(count.try_into()?);
|
||||
for _ in 0..count {
|
||||
ans.push(T::read_primitive(the_type, file)?);
|
||||
}
|
||||
ans.try_into().map_err(|_| TiffError::InvalidCount)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TypeString;
|
||||
pub struct TypeSonyToneCurve;
|
||||
pub struct TypeOrientation;
|
||||
|
||||
impl TagType for TypeString {
|
||||
type Output = String;
|
||||
|
||||
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let string = Array::<TypeAscii>::read(file)?;
|
||||
|
||||
// Skip the NUL character at the end
|
||||
let len = string.len();
|
||||
Ok(string.into_iter().take(len - 1).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl TagType for TypeSonyToneCurve {
|
||||
type Output = CurveLookupTable;
|
||||
|
||||
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
let values = ConstArray::<TypeShort, 4>::read(file)?;
|
||||
Ok(CurveLookupTable::from_sony_tone_table(values))
|
||||
}
|
||||
}
|
||||
|
||||
impl TagType for TypeOrientation {
|
||||
type Output = Transform;
|
||||
|
||||
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
|
||||
Ok(match TypeShort::read(file)? {
|
||||
1 => Transform::Horizontal,
|
||||
2 => Transform::MirrorHorizontal,
|
||||
3 => Transform::Rotate180,
|
||||
4 => Transform::MirrorVertical,
|
||||
5 => Transform::MirrorHorizontalRotate270,
|
||||
6 => Transform::Rotate90,
|
||||
7 => Transform::MirrorHorizontalRotate90,
|
||||
8 => Transform::Rotate270,
|
||||
_ => return Err(TiffError::InvalidValue),
|
||||
})
|
||||
}
|
||||
}
|
||||
79
libraries/rawkit/src/tiff/values.rs
Normal file
79
libraries/rawkit/src/tiff/values.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
pub trait ToFloat {
|
||||
fn to_float(&self) -> f64;
|
||||
}
|
||||
|
||||
impl ToFloat for u32 {
|
||||
fn to_float(&self) -> f64 {
|
||||
*self as f64
|
||||
}
|
||||
}
|
||||
|
||||
impl ToFloat for i32 {
|
||||
fn to_float(&self) -> f64 {
|
||||
*self as f64
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rational<T: ToFloat> {
|
||||
pub numerator: T,
|
||||
pub denominator: T,
|
||||
}
|
||||
|
||||
impl<T: ToFloat> ToFloat for Rational<T> {
|
||||
fn to_float(&self) -> f64 {
|
||||
self.numerator.to_float() / self.denominator.to_float()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CurveLookupTable {
|
||||
table: Vec<u16>,
|
||||
}
|
||||
|
||||
impl CurveLookupTable {
|
||||
pub fn from_sony_tone_table(values: [u16; 4]) -> CurveLookupTable {
|
||||
let mut sony_curve = [0, 0, 0, 0, 0, 4095];
|
||||
for i in 0..4 {
|
||||
sony_curve[i + 1] = values[i] >> 2 & 0xfff;
|
||||
}
|
||||
|
||||
let mut table = vec![0_u16; (sony_curve[5] + 1).into()];
|
||||
for i in 0..5 {
|
||||
for j in (sony_curve[i] + 1)..=sony_curve[i + 1] {
|
||||
table[j as usize] = table[(j - 1) as usize] + (1 << i);
|
||||
}
|
||||
}
|
||||
|
||||
CurveLookupTable { table }
|
||||
}
|
||||
|
||||
pub fn get(&self, x: usize) -> u16 {
|
||||
self.table[x]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum Transform {
|
||||
Horizontal,
|
||||
MirrorHorizontal,
|
||||
Rotate180,
|
||||
MirrorVertical,
|
||||
MirrorHorizontalRotate270,
|
||||
Rotate90,
|
||||
MirrorHorizontalRotate90,
|
||||
Rotate270,
|
||||
}
|
||||
|
||||
impl Transform {
|
||||
pub fn is_identity(&self) -> bool {
|
||||
*self == Transform::Horizontal
|
||||
}
|
||||
|
||||
pub fn will_swap_coordinates(&self) -> bool {
|
||||
use Transform as Tr;
|
||||
|
||||
match *self {
|
||||
Tr::Horizontal | Tr::MirrorHorizontal | Tr::Rotate180 | Tr::MirrorVertical => false,
|
||||
Tr::MirrorHorizontalRotate270 | Tr::Rotate90 | Tr::MirrorHorizontalRotate90 | Tr::Rotate270 => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user