Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions examples/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ fn main() {
let from = if env::args_os().count() == 2 {
env::args_os().nth(1).unwrap()
} else {
println!("Please enter a from and into path.");
println!("Please enter the path to an image file.");
std::process::exit(1);
};

// Use the open function to load an image from a Path.
// ```open``` returns a dynamic image.
let im = image::open(Path::new(&from)).unwrap();
println!("{}", im.as_bytes().len());
println!(
"size: {} x {} ({} bytes), type {:?}",
im.width(),
im.height(),
im.as_bytes().len(),
im.color(),
);
println!("color space: {:?}", im.color_space());
}
18 changes: 17 additions & 1 deletion src/codecs/avif/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use crate::error::{
DecodingError, ImageFormatHint, LimitError, LimitErrorKind, UnsupportedError,
UnsupportedErrorKind,
};
use crate::io::{DecodedImageAttributes, DecoderPreparedImage};
use crate::io::{
DecodedColorProfile, DecodedImageAttributes, DecodedMetadataHint, DecoderPreparedImage,
FormatAttributes,
};
use crate::metadata::Orientation;
use crate::{ColorType, ImageDecoder, ImageError, ImageFormat, ImageResult, Limits};
///
Expand Down Expand Up @@ -411,6 +414,14 @@ fn get_matrix(
}

impl<R: Read> ImageDecoder for AvifDecoder<R> {
fn format_attributes(&self) -> FormatAttributes {
FormatAttributes {
icc: DecodedMetadataHint::InHeader,
color_profile: DecodedMetadataHint::InHeader,
..FormatAttributes::default()
}
}

fn set_limits(&mut self, limits: Limits) -> ImageResult<()> {
self.limits = limits;
Ok(())
Expand All @@ -434,6 +445,11 @@ impl<R: Read> ImageDecoder for AvifDecoder<R> {
Ok(self.icc_profile.clone())
}

fn color_profile(&mut self) -> ImageResult<Option<DecodedColorProfile>> {
// TODO: read NCLX once the next mp4parse version exposes it
Ok(self.icc_profile()?.map(DecodedColorProfile::from_icc))
}

fn read_image(&mut self, buf: &mut [u8]) -> ImageResult<DecodedImageAttributes> {
let prepared = self.prepare_image()?;
assert_eq!(u64::try_from(buf.len()), Ok(prepared.total_bytes()));
Expand Down
164 changes: 99 additions & 65 deletions src/codecs/bmp/decoder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::io::DecoderPreparedImage;
use crate::io::{DecodedColorProfile, DecodedMetadataHint, DecoderPreparedImage, FormatAttributes};
use crate::metadata::Cicp;
use crate::utils::vec_try_with_capacity;
use std::cmp::{self, Ordering};
use std::io::{self, BufRead, Seek, SeekFrom};
Expand Down Expand Up @@ -284,7 +285,7 @@ impl ParsedIccProfile {
}

/// Color space data parsed from V4/V5 BMP headers.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ColorSpaceInfo {
/// LCS_CALIBRATED_RGB: endpoint and gamma values specified in the header.
CalibratedRgb(CalibratedRgb),
Expand All @@ -309,32 +310,30 @@ impl ColorSpaceInfo {
let read_u32 = |offset: usize| -> u32 {
u32::from_le_bytes(buffer[offset..offset + 4].try_into().unwrap())
};

// FXPT2DOT30 (2.30 fixed-point) → f32.
let fxpt2dot30 = |val: u32| -> f32 { val as f32 * (1.0 / (1u64 << 30) as f32) };
// FXPT16DOT16 (16.16 fixed-point) → f32.
let fxpt16dot16 = |val: u32| -> f32 { val as f32 / 65536.0 };
let read_i32 = |offset: usize| -> i32 {
i32::from_le_bytes(buffer[offset..offset + 4].try_into().unwrap())
};

// CIEXYZTRIPLE: 9 FXPT2DOT30 values at offsets 60-95 from header
// start (56-91 from after size field). Layout:
// RedX, RedY, RedZ, GreenX, GreenY, GreenZ, BlueX, BlueY, BlueZ
// We read only X and Y per primary (Z is implicit: Z = 1 - X - Y
// for chromaticity, but BMP stores raw CIE XYZ values).
let rx = fxpt2dot30(read_u32(56));
let ry = fxpt2dot30(read_u32(60));
let gx = fxpt2dot30(read_u32(68));
let gy = fxpt2dot30(read_u32(72));
let bx = fxpt2dot30(read_u32(80));
let by = fxpt2dot30(read_u32(84));
let rx = Fxpt2Dot30(read_i32(56));
let ry = Fxpt2Dot30(read_i32(60));
let gx = Fxpt2Dot30(read_i32(68));
let gy = Fxpt2Dot30(read_i32(72));
let bx = Fxpt2Dot30(read_i32(80));
let by = Fxpt2Dot30(read_i32(84));

// Gamma values at offsets 96-107 from header start (92-103 from after size).
let gamma_r = fxpt16dot16(read_u32(92));
let gamma_g = fxpt16dot16(read_u32(96));
let gamma_b = fxpt16dot16(read_u32(100));
let gamma_r = Fxpt16Dot16(read_u32(92));
let gamma_g = Fxpt16Dot16(read_u32(96));
let gamma_b = Fxpt16Dot16(read_u32(100));

// Validate: Y values must be non-zero (used as denominators in
// XYZ→chromaticity conversion by color management libraries).
if ry == 0.0 || gy == 0.0 || by == 0.0 {
if ry == Fxpt2Dot30(0) || gy == Fxpt2Dot30(0) || by == Fxpt2Dot30(0) {
return None;
}

Expand All @@ -359,41 +358,61 @@ impl ColorSpaceInfo {
}
}

/// FXPT2DOT30, 2.30 fixed-point
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Fxpt2Dot30(i32);

impl Fxpt2Dot30 {
fn to_float(self) -> f32 {
self.0 as f32 * (1.0 / (1u64 << 30) as f32)
}
}

/// FXPT16DOT16, unsigned 16.16 fixed-point
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Fxpt16Dot16(u32);

impl Fxpt16Dot16 {
fn to_float(self) -> f32 {
self.0 as f32 / 65536.0
}
}

/// Calibrated RGB color space parameters from a BMP V4/V5 header.
///
/// When the header's `bV4CSType` is `LCS_CALIBRATED_RGB`, these fields
/// carry the CIE XYZ endpoint coordinates for the RGB primaries and
/// per-channel gamma values, parsed from the FXPT2DOT30 / FXPT16DOT16
/// per-channel gamma values, stored as the raw FXPT2DOT30 / FXPT16DOT16
/// fixed-point fields in the header.
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CalibratedRgb {
/// Red primary CIE X coordinate (FXPT2DOT30).
rx: f32,
rx: Fxpt2Dot30,
/// Red primary CIE Y coordinate (FXPT2DOT30).
ry: f32,
ry: Fxpt2Dot30,
/// Green primary CIE X coordinate (FXPT2DOT30).
gx: f32,
gx: Fxpt2Dot30,
/// Green primary CIE Y coordinate (FXPT2DOT30).
gy: f32,
gy: Fxpt2Dot30,
/// Blue primary CIE X coordinate (FXPT2DOT30).
bx: f32,
bx: Fxpt2Dot30,
/// Blue primary CIE Y coordinate (FXPT2DOT30).
by: f32,
/// Red channel gamma (FXPT16DOT16).
gamma_r: f32,
/// Green channel gamma (FXPT16DOT16).
gamma_g: f32,
/// Blue channel gamma (FXPT16DOT16).
gamma_b: f32,
by: Fxpt2Dot30,
/// Red channel gamma (unsigned FXPT16DOT16).
gamma_r: Fxpt16Dot16,
/// Green channel gamma (unsigned FXPT16DOT16).
gamma_g: Fxpt16Dot16,
/// Blue channel gamma (unsigned FXPT16DOT16).
gamma_b: Fxpt16Dot16,
}

impl CalibratedRgb {
/// Build a moxcms `ColorProfile` from the calibrated RGB primaries and gamma.
fn to_color_profile(self) -> moxcms::ColorProfile {
let primaries = moxcms::ColorPrimaries {
red: moxcms::Chromaticity::new(self.rx, self.ry),
green: moxcms::Chromaticity::new(self.gx, self.gy),
blue: moxcms::Chromaticity::new(self.bx, self.by),
red: moxcms::Chromaticity::new(self.rx.to_float(), self.ry.to_float()),
green: moxcms::Chromaticity::new(self.gx.to_float(), self.gy.to_float()),
blue: moxcms::Chromaticity::new(self.bx.to_float(), self.by.to_float()),
};

let mut profile = moxcms::ColorProfile::new_srgb();
Expand All @@ -407,9 +426,9 @@ impl CalibratedRgb {
// when serialised to ICC bytes.
let safe_gamma = |g: f32| if g > 0.0 { g } else { 1.0 };
let parametric_trc = |g: f32| moxcms::ToneReprCurve::Parametric(vec![safe_gamma(g)]);
profile.red_trc = Some(parametric_trc(self.gamma_r));
profile.green_trc = Some(parametric_trc(self.gamma_g));
profile.blue_trc = Some(parametric_trc(self.gamma_b));
profile.red_trc = Some(parametric_trc(self.gamma_r.to_float()));
profile.green_trc = Some(parametric_trc(self.gamma_g.to_float()));
profile.blue_trc = Some(parametric_trc(self.gamma_b.to_float()));
profile
}
}
Expand Down Expand Up @@ -448,7 +467,7 @@ enum MetadataProgress {
ReadingPalette { offsets: HeaderOffsets },
/// Headers and palette (if any) have been read; now reading ICC profile.
/// Stores header offsets for the ICC profile read.
ReadingIccProfile { offsets: HeaderOffsets },
ReadingIccProfile,
/// All metadata has been read successfully.
Complete,
}
Expand All @@ -462,8 +481,6 @@ struct HeaderOffsets {
bmp_header_end: u64,
/// Offset where palette data starts (after headers).
palette_offset: u64,
/// ICC profile metadata if present.
icc_profile: Option<ParsedIccProfile>,
}

/// Progress within the RLE decoding phase.
Expand Down Expand Up @@ -1103,6 +1120,7 @@ pub struct BmpDecoder<R> {
palette: Option<Vec<[u8; 3]>>,
bitfields: Option<Bitfields>,
icc_profile: Option<Vec<u8>>,
color_space_info: Option<ColorSpaceInfo>,
spec_strictness: SpecCompliance,

/// Current decoder state for resumable decoding.
Expand All @@ -1129,6 +1147,7 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
colors_used: 0,
palette: None,
bitfields: None,
color_space_info: None,
icc_profile: None,
spec_strictness: SpecCompliance::default(),
state: DecoderState::default(),
Expand Down Expand Up @@ -1457,7 +1476,7 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
}

/// Read ICC profile data from the file.
fn read_icc_profile(&mut self, icc: &ParsedIccProfile) -> ImageResult<()> {
fn read_icc_profile(&mut self, icc: ParsedIccProfile) -> ImageResult<()> {
let profile_end = icc
.profile_offset
.checked_add(u64::from(icc.profile_size))
Expand Down Expand Up @@ -1571,13 +1590,13 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
}

// Always progress to ReadingIccProfile next
let next = MetadataProgress::ReadingIccProfile { offsets };
let next = MetadataProgress::ReadingIccProfile;
self.state = DecoderState::ReadingMetadata { progress: next };
self.read_metadata_impl(next)
}
MetadataProgress::ReadingIccProfile { offsets } => {
MetadataProgress::ReadingIccProfile => {
// Read ICC profile if present
if let Some(ref icc) = offsets.icc_profile {
if let Some(ColorSpaceInfo::EmbeddedIcc(icc)) = self.color_space_info {
self.read_icc_profile(icc)?;
}

Expand Down Expand Up @@ -1696,7 +1715,6 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
};

// Parse color space fields from V4/V5 header
let mut icc_profile = None;
if bmp_header_size >= BITMAPV4HEADER_SIZE {
// Read the header into a buffer for color space parsing.
// Buffer starts after the 4-byte size field.
Expand All @@ -1706,21 +1724,8 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
self.reader.read_exact(&mut header_buffer)?;

// Extract color space info and handle non-Copy variants immediately
match ColorSpaceInfo::parse(&header_buffer, bmp_header_size, bmp_header_offset) {
Some(ColorSpaceInfo::CalibratedRgb(params)) => {
// Synthesize an ICC profile from the calibrated RGB parameters
// and store it directly — no file read needed.
if let Ok(encoded) = params.to_color_profile().encode() {
self.icc_profile = Some(encoded);
}
}
Some(ColorSpaceInfo::EmbeddedIcc(icc)) => {
icc_profile = Some(icc);
}
// LCS_sRGB / LCS_WINDOWS_COLOR_SPACE: the caller treats
// "no ICC profile" as sRGB, so nothing to store.
Some(ColorSpaceInfo::Srgb) | None => {}
}
self.color_space_info =
ColorSpaceInfo::parse(&header_buffer, bmp_header_size, bmp_header_offset);

// Seek back to where we were
self.reader.seek(SeekFrom::Start(current_pos))?;
Expand All @@ -1732,7 +1737,6 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
Ok(HeaderOffsets {
bmp_header_end,
palette_offset,
icc_profile,
})
}

Expand Down Expand Up @@ -2445,6 +2449,14 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
}

impl<R: BufRead + Seek> ImageDecoder for BmpDecoder<R> {
fn format_attributes(&self) -> FormatAttributes {
FormatAttributes {
icc: DecodedMetadataHint::InHeader,
color_profile: DecodedMetadataHint::InHeader,
..FormatAttributes::default()
}
}

fn prepare_image(&mut self) -> ImageResult<DecoderPreparedImage> {
let color = if self.indexed_color {
ColorType::L8
Expand All @@ -2465,6 +2477,28 @@ impl<R: BufRead + Seek> ImageDecoder for BmpDecoder<R> {
Ok(self.icc_profile.clone())
}

fn color_profile(&mut self) -> ImageResult<Option<DecodedColorProfile>> {
match self.color_space_info {
Some(ColorSpaceInfo::CalibratedRgb(params)) => {
// Synthesize an ICC profile from the calibrated RGB parameters
// and store it directly — no file read needed.
Ok(Some(DecodedColorProfile::from_icc(
params
.to_color_profile()
.encode()
.expect("synthetic profile should always succeed"),
)))
}
Some(ColorSpaceInfo::EmbeddedIcc(_)) => Ok(Some(DecodedColorProfile::from_icc(
self.icc_profile.clone().expect("icc was loaded"),
))),
Some(ColorSpaceInfo::Srgb) => {
Ok(Some(DecodedColorProfile::from_plain_cicp(Cicp::SRGB)))
}
None => Ok(None),
}
}

fn read_image(&mut self, buf: &mut [u8]) -> ImageResult<DecodedImageAttributes> {
let layout = self.prepare_image()?;
assert_eq!(u64::try_from(buf.len()), Ok(layout.total_bytes()));
Expand Down Expand Up @@ -2588,6 +2622,7 @@ mod test {
let mut decoder = BmpDecoder::new(f).unwrap();
let profile = decoder.icc_profile().unwrap();
assert!(profile.is_some());
assert!(profile == decoder.color_profile().unwrap().unwrap().to_icc().unwrap());
let profile_data = profile.unwrap();
assert_eq!(profile_data.len(), 3048);
validate_icc_profile(
Expand All @@ -2602,6 +2637,7 @@ mod test {
let mut decoder = BmpDecoder::new(f).unwrap();
let profile = decoder.icc_profile().unwrap();
assert!(profile.is_some());
assert!(profile == decoder.color_profile().unwrap().unwrap().to_icc().unwrap());
let profile_data = profile.unwrap();
assert_eq!(profile_data.len(), 540);
validate_icc_profile(
Expand All @@ -2617,7 +2653,7 @@ mod test {
// pal8v4.bmp has a V4 header with LCS_CALIBRATED_RGB — should synthesize an ICC profile.
let data = std::fs::read("tests/images/bmp/images/pal8v4.bmp").unwrap();
let mut decoder = BmpDecoder::new(Cursor::new(&data)).unwrap();
let profile = decoder.icc_profile().unwrap();
let profile = decoder.color_profile().unwrap().unwrap().to_icc().unwrap();
assert!(
profile.is_some(),
"pal8v4: should have a synthesized ICC profile from calibrated RGB parameters"
Expand Down Expand Up @@ -2843,9 +2879,7 @@ mod test {
MetadataProgress::ReadingPalette { .. } => {
saw_reading_palette = true
}
MetadataProgress::ReadingIccProfile { .. } => {
saw_reading_icc = true
}
MetadataProgress::ReadingIccProfile => saw_reading_icc = true,
_ => {}
}
}
Expand Down
Loading
Loading