Add basic thumbnail extraction to Rawkit (#2659)

* Add `CompressionValue`

* Rename `Transform` to `OrientationValue`

* Add basic thumbnail extraction

* Add `ThumbnailFormat`

* fmt + clippy fixes
This commit is contained in:
Dr George Atkinson 2025-12-21 02:08:21 +00:00 committed by GitHub
parent d441a02e72
commit c21ccf5284
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 171 additions and 80 deletions

View File

@ -1,7 +1,7 @@
use crate::tiff::Ifd;
use crate::tiff::file::TiffRead;
use crate::tiff::tags::SonyDataOffset;
use crate::{RawImage, SubtractBlack, Transform};
use crate::{OrientationValue, RawImage, SubtractBlack};
use bitstream_io::{BE, BitRead, BitReader, Endianness};
use std::io::{Read, Seek};
@ -25,7 +25,7 @@ pub fn decode_a100<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage
#[allow(unreachable_code)]
maximum: (1 << 12) - 1,
black: SubtractBlack::None,
transform: Transform::Horizontal,
orientation: OrientationValue::Horizontal,
camera_model: None,
camera_white_balance: None,
white_balance: None,

View File

@ -1,8 +1,8 @@
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::values::{CompressionValue, CurveLookupTable};
use crate::tiff::{Ifd, TiffError};
use crate::{RawImage, SubtractBlack, Transform};
use crate::{OrientationValue, RawImage, SubtractBlack};
use rawkit_proc_macros::Tag;
use std::io::{Read, Seek};
@ -26,7 +26,7 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
assert!(ifd.strip_offsets.len() == ifd.strip_byte_counts.len());
assert!(ifd.strip_offsets.len() == 1);
assert!(ifd.compression == 32767);
assert!(ifd.compression == CompressionValue::Sony_ARW_Compressed);
let image_width: usize = ifd.image_width.try_into().unwrap();
let image_height: usize = ifd.image_height.try_into().unwrap();
@ -49,7 +49,7 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
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,
orientation: OrientationValue::Horizontal,
camera_model: None,
camera_white_balance: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)),
white_balance: None,

View File

@ -1,7 +1,8 @@
use crate::tiff::file::TiffRead;
use crate::tiff::tags::{BitsPerSample, BlackLevel, CfaPattern, CfaPatternDim, Compression, ImageLength, ImageWidth, RowsPerStrip, StripByteCounts, StripOffsets, Tag, WhiteBalanceRggbLevels};
use crate::tiff::values::CompressionValue;
use crate::tiff::{Ifd, TiffError};
use crate::{RawImage, SubtractBlack, Transform};
use crate::{OrientationValue, RawImage, SubtractBlack};
use rawkit_proc_macros::Tag;
use std::io::{Read, Seek};
@ -26,7 +27,7 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
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
assert!(ifd.compression == CompressionValue::Uncompressed);
let image_width: usize = ifd.image_width.try_into().unwrap();
let image_height: usize = ifd.image_height.try_into().unwrap();
@ -57,7 +58,7 @@ pub fn decode<R: Read + Seek>(ifd: Ifd, file: &mut TiffRead<R>) -> RawImage {
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,
orientation: OrientationValue::Horizontal,
camera_model: None,
camera_white_balance: ifd.white_balance_levels.map(|arr| arr.map(|x| x as f64)),
white_balance: None,

View File

@ -12,13 +12,24 @@ use rawkit_proc_macros::Tag;
use std::io::{Read, Seek};
use thiserror::Error;
use tiff::file::TiffRead;
use tiff::tags::{Compression, ImageLength, ImageWidth, Orientation, StripByteCounts, SubIfd, Tag};
use tiff::values::Transform;
use tiff::tags::{Compression, ImageLength, ImageWidth, Orientation, StripByteCounts, SubIfd, Tag, ThumbnailLength, ThumbnailOffset};
use tiff::values::{CompressionValue, OrientationValue};
use tiff::{Ifd, TiffError};
pub(crate) const CHANNELS_IN_RGB: usize = 3;
pub(crate) type Histogram = [[usize; 0x2000]; CHANNELS_IN_RGB];
pub enum ThumbnailFormat {
Jpeg,
Unsupported,
}
/// A thumbnail image extracted from the raw file. This is usually a JPEG image.
pub struct ThumbnailImage {
pub data: Vec<u8>,
pub format: ThumbnailFormat,
}
/// The amount of black level to be subtracted from Raw Image.
pub enum SubtractBlack {
/// Don't subtract any value.
@ -48,7 +59,7 @@ pub struct RawImage {
pub cfa_pattern: [u8; 4],
/// Transformation to be applied to negate the orientation of camera.
pub transform: Transform,
pub orientation: OrientationValue,
/// The maximum possible value of pixel that the camera sensor could give.
pub maximum: u16,
@ -97,8 +108,8 @@ pub struct Image<T> {
/// The transformation required to orient the image correctly.
///
/// This will be [`Transform::Horizontal`] after the transform step is applied.
pub transform: Transform,
/// This will be [`OrientationValue::Horizontal`] after the orientation step is applied.
pub orientation: OrientationValue,
}
#[allow(dead_code)]
@ -119,7 +130,7 @@ impl RawImage {
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 orientation = ifd.get_value::<Orientation, _>(&mut file)?;
let mut raw_image = if camera_model.model == "DSLR-A100" {
decoder::arw1::decode_a100(ifd, &mut file)
@ -127,7 +138,7 @@ impl RawImage {
let sub_ifd = ifd.get_value::<SubIfd, _>(&mut file)?;
let arw_ifd = sub_ifd.get_value::<ArwIfd, _>(&mut file)?;
if arw_ifd.compression == 1 {
if arw_ifd.compression == CompressionValue::Uncompressed {
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)
@ -138,13 +149,38 @@ impl RawImage {
};
raw_image.camera_model = Some(camera_model);
raw_image.transform = transform;
raw_image.orientation = orientation;
raw_image.calculate_conversion_matrices();
Ok(raw_image)
}
/// Extracts the thumbnail image from the raw file.
pub fn extract_thumbnail<R: Read + Seek>(reader: &mut R) -> Result<ThumbnailImage, DecoderError> {
let mut file = TiffRead::new(reader)?;
let ifd = Ifd::new_first_ifd(&mut file)?;
// TODO: ARW files Store the thumbnail offset and length in the first IFD. Add support for other file types in the future.
let thumbnail_offset = ifd.get_value::<ThumbnailOffset, _>(&mut file)?;
let thumbnail_length = ifd.get_value::<ThumbnailLength, _>(&mut file)?;
file.seek_from_start(thumbnail_offset)?;
let mut thumbnail_data = vec![0; thumbnail_length as usize];
file.read_exact(&mut thumbnail_data)?;
// Check the first two bytes to determine the format of the thumbnail.
// JPEG format starts with 0xFF, 0xD8.
if thumbnail_data[0..2] == [0xFF, 0xD8] {
Ok(ThumbnailImage {
data: thumbnail_data,
format: ThumbnailFormat::Jpeg,
})
} else {
Err(DecoderError::UnsupportedThumbnailFormat)
}
}
/// Converts the [`RawImage`] to an [`Image`] with 8 bit resolution for each channel.
///
/// Applies all the processing steps to finally get RGB pixel data.
@ -156,7 +192,7 @@ impl RawImage {
data: image.data.iter().map(|x| (x >> 8) as u8).collect(),
width: image.width,
height: image.height,
transform: image.transform,
orientation: image.orientation,
}
}
@ -174,7 +210,7 @@ impl RawImage {
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 {
if image.orientation == OrientationValue::Horizontal {
image.apply(gamma_correction)
} else {
image.transform_and_apply(gamma_correction)
@ -211,7 +247,7 @@ impl RawImage {
data: image,
width: self.width,
height: self.height,
transform: self.transform,
orientation: self.orientation,
}
}
}
@ -232,7 +268,7 @@ impl Image<u16> {
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();
let (width, height, iter) = self.orientation_iter();
for Pixel { values, row, column } in iter.map(|mut pixel| {
pixel.values = transform.apply(pixel);
pixel
@ -246,7 +282,7 @@ impl Image<u16> {
data: image,
width,
height,
transform: Transform::Horizontal,
orientation: OrientationValue::Horizontal,
}
}
}
@ -259,4 +295,6 @@ pub enum DecoderError {
ConversionError(#[from] std::num::TryFromIntError),
#[error("An IO Error ocurred")]
IoError(#[from] std::io::Error),
#[error("The thumbnail format is unsupported")]
UnsupportedThumbnailFormat,
}

View File

@ -1,16 +1,16 @@
use crate::{Image, Pixel, Transform};
use crate::{Image, OrientationValue, Pixel};
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() {
pub fn orientation_iter(&self) -> (usize, usize, impl Iterator<Item = Pixel> + use<'_>) {
let (final_width, final_height) = if self.orientation.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 index_0_0 = inverse_orientation_index(self.orientation, 0, 0, self.width, self.height);
let index_0_1 = inverse_orientation_index(self.orientation, 0, 1, self.width, self.height);
let index_1_0 = inverse_orientation_index(self.orientation, 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);
@ -42,16 +42,16 @@ impl Image<u16> {
}
}
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,
pub fn inverse_orientation_index(orientation: OrientationValue, mut row: usize, mut column: usize, width: usize, height: usize) -> (i64, i64) {
let value = match orientation {
OrientationValue::Horizontal => 0,
OrientationValue::MirrorHorizontal => 1,
OrientationValue::Rotate180 => 3,
OrientationValue::MirrorVertical => 2,
OrientationValue::MirrorHorizontalRotate270 => 4,
OrientationValue::Rotate90 => 6,
OrientationValue::MirrorHorizontalRotate90 => 7,
OrientationValue::Rotate270 => 5,
};
if value & 4 != 0 {

View File

@ -26,8 +26,8 @@ pub enum TagId {
RowsPerStrip = 0x116,
StripByteCounts = 0x117,
SubIfd = 0x14a,
JpegOffset = 0x201,
JpegLength = 0x202,
ThumbnailOffset = 0x201,
ThumbnailLength = 0x202,
SonyToneCurve = 0x7010,
BlackLevel = 0x7310,
WhiteBalanceRggbLevels = 0x7313,
@ -88,10 +88,10 @@ impl Ifd {
}
file.seek_from_start(offset)?;
let num = file.read_u16()?;
let num_entries = file.read_u16()?;
let mut ifd_entries = Vec::with_capacity(num.into());
for _ in 0..num {
let mut ifd_entries = Vec::with_capacity(num_entries.into());
for _ in 0..num_entries {
let tag = file.read_u16()?.into();
let the_type = file.read_u16()?.into();
let count = file.read_u32()?;

View File

@ -1,4 +1,4 @@
use super::types::{Array, ConstArray, TagType, TypeByte, TypeIfd, TypeLong, TypeNumber, TypeOrientation, TypeSRational, TypeSShort, TypeShort, TypeSonyToneCurve, TypeString};
use super::types::{Array, ConstArray, TagType, TypeByte, TypeCompression, TypeIfd, TypeLong, TypeNumber, TypeOrientation, TypeSRational, TypeSShort, TypeShort, TypeSonyToneCurve, TypeString};
use super::{Ifd, TagId, TiffError, TiffRead};
use std::io::{Read, Seek};
@ -22,8 +22,8 @@ pub struct SamplesPerPixel;
pub struct RowsPerStrip;
pub struct StripByteCounts;
pub struct SubIfd;
pub struct JpegOffset;
pub struct JpegLength;
pub struct ThumbnailOffset;
pub struct ThumbnailLength;
pub struct SonyDataOffset;
pub struct SonyToneCurve;
pub struct BlackLevel;
@ -55,7 +55,7 @@ impl SimpleTag for BitsPerSample {
}
impl SimpleTag for Compression {
type Type = TypeShort;
type Type = TypeCompression;
const ID: TagId = TagId::Compression;
const NAME: &'static str = "Compression";
@ -124,17 +124,17 @@ impl SimpleTag for SubIfd {
const NAME: &'static str = "SubIFD";
}
impl SimpleTag for JpegOffset {
impl SimpleTag for ThumbnailOffset {
type Type = TypeLong;
const ID: TagId = TagId::JpegOffset;
const ID: TagId = TagId::ThumbnailOffset;
const NAME: &'static str = "Jpeg Offset";
}
impl SimpleTag for JpegLength {
impl SimpleTag for ThumbnailLength {
type Type = TypeLong;
const ID: TagId = TagId::JpegLength;
const ID: TagId = TagId::ThumbnailLength;
const NAME: &'static str = "Jpeg Length";
}

View File

@ -1,5 +1,5 @@
use super::file::TiffRead;
use super::values::{CurveLookupTable, Rational, Transform};
use super::values::{CompressionValue, CurveLookupTable, OrientationValue, Rational};
use super::{Ifd, IfdTagType, TiffError};
use std::io::{Read, Seek};
@ -350,6 +350,7 @@ impl<T: PrimitiveType, const N: usize> TagType for ConstArray<T, N> {
}
}
pub struct TypeCompression;
pub struct TypeString;
pub struct TypeSonyToneCurve;
pub struct TypeOrientation;
@ -376,19 +377,17 @@ impl TagType for TypeSonyToneCurve {
}
impl TagType for TypeOrientation {
type Output = Transform;
type Output = OrientationValue;
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),
})
OrientationValue::try_from(TypeShort::read(file)?).map_err(|_| TiffError::InvalidValue)
}
}
impl TagType for TypeCompression {
type Output = CompressionValue;
fn read<R: Read + Seek>(file: &mut TiffRead<R>) -> Result<Self::Output, TiffError> {
CompressionValue::try_from(TypeShort::read(file)?).map_err(|_| TiffError::InvalidValue)
}
}

View File

@ -1,3 +1,5 @@
use num_enum::{IntoPrimitive, TryFromPrimitive};
pub trait ToFloat {
fn to_float(&self) -> f64;
}
@ -51,29 +53,80 @@ impl CurveLookupTable {
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum Transform {
Horizontal,
MirrorHorizontal,
Rotate180,
MirrorVertical,
MirrorHorizontalRotate270,
Rotate90,
MirrorHorizontalRotate90,
Rotate270,
#[derive(Copy, Clone, Eq, PartialEq, Debug, IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
pub enum OrientationValue {
Horizontal = 1,
MirrorHorizontal = 2,
Rotate180 = 3,
MirrorVertical = 4,
MirrorHorizontalRotate270 = 5,
Rotate90 = 6,
MirrorHorizontalRotate90 = 7,
Rotate270 = 8,
}
impl Transform {
impl OrientationValue {
pub fn is_identity(&self) -> bool {
*self == Transform::Horizontal
*self == Self::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,
Self::Horizontal | Self::MirrorHorizontal | Self::Rotate180 | Self::MirrorVertical => false,
Self::MirrorHorizontalRotate270 | Self::Rotate90 | Self::MirrorHorizontalRotate90 | Self::Rotate270 => true,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
#[allow(non_camel_case_types)]
pub enum CompressionValue {
Uncompressed = 1,
CCITT_1D = 2,
T4 = 3,
T6 = 4,
LZW = 5,
JPEG_Old = 6,
JPEG = 7,
AdobeDeflate = 8,
JBIG_BW = 9,
JBIG_Color = 10,
KODAK_626 = 262,
Next = 32766,
Sony_ARW_Compressed = 32767,
Packed_Raw = 32769,
Samsung_SRW_Compressed = 32770,
CCIRLEW = 32771,
Samsung_SRW_Compressed_2 = 32772,
PackedBits = 32773,
Thunderscan = 32809,
Kodak_KDC_Compressed = 32867,
IT8CTPAD = 32895,
IT8LW = 32896,
IT8MP = 32897,
IT8BL = 32898,
PixarFilm = 32908,
PixarLog = 32909,
Deflate = 32946,
DCS = 32947,
AperioJPEG2K_YCbCr = 33003,
AperioJPEG2K_RGB = 33005,
JBIG = 34661,
SGILog = 34676,
SGILog24 = 34677,
JPEG2K = 34712,
NikonNEFCompressed = 34713,
JBIG2_TIFF_FX = 34715,
ESRI_Lerc = 34887,
LossyJPEG = 34892,
LZMA2 = 34925,
PNG = 34933,
JPEG_XR = 34934,
Zstd = 50000,
WebP = 50001,
JPEG_XL = 52546,
Kodak_DCR_Compressed = 65000,
Pentax_PEF_Compressed = 65535,
}