Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement writing ICC profiles for JPEG and PNG images. #2389

Merged
merged 2 commits into from
Dec 16, 2024
Merged
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
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ num-traits = { version = "0.2.0" }
color_quant = { version = "1.1", optional = true }
dav1d = { version = "0.10.3", optional = true }
exr = { version = "1.5.0", optional = true }
gif = { version = "0.13", optional = true }
gif = { version = "0.13.1", optional = true }
image-webp = { version = "0.2.0", optional = true }
mp4parse = { version = "0.17.0", optional = true }
png = { version = "0.17.6", optional = true }
png = { version = "0.17.11", optional = true }
qoi = { version = "0.4", optional = true }
ravif = { version = "0.11.11", default-features = false, optional = true }
rayon = { version = "1.7.0", optional = true }
Expand Down Expand Up @@ -114,4 +114,4 @@ harness = false
[[bench]]
path = "benches/blur.rs"
name = "blur"
harness = false
harness = false
50 changes: 50 additions & 0 deletions src/codecs/jpeg/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ static SOS: u8 = 0xDA;
static DQT: u8 = 0xDB;
// Application segments start and end
static APP0: u8 = 0xE0;
static APP2: u8 = 0xE2;

// section K.1
// table K.1
Expand Down Expand Up @@ -346,6 +347,8 @@ pub struct JpegEncoder<W> {
chroma_actable: Cow<'static, [(u8, u16); 256]>,

pixel_density: PixelDensity,

icc_profile: Vec<u8>,
}

impl<W: Write> JpegEncoder<W> {
Expand Down Expand Up @@ -415,6 +418,8 @@ impl<W: Write> JpegEncoder<W> {
chroma_actable: Cow::Borrowed(&STD_CHROMA_AC_HUFF_LUT),

pixel_density: PixelDensity::default(),

icc_profile: Vec::new(),
}
}

Expand Down Expand Up @@ -494,6 +499,9 @@ impl<W: Write> JpegEncoder<W> {
build_jfif_header(&mut buf, self.pixel_density);
self.writer.write_segment(APP0, &buf)?;

// Write ICC profile chunks if present
self.write_icc_profile_chunks()?;

build_frame_header(
&mut buf,
8,
Expand Down Expand Up @@ -648,6 +656,43 @@ impl<W: Write> JpegEncoder<W> {

Ok(())
}

fn write_icc_profile_chunks(&mut self) -> io::Result<()> {
if self.icc_profile.is_empty() {
return Ok(());
}

const MAX_CHUNK_SIZE: usize = 65533 - 14;
const MAX_CHUNK_COUNT: usize = 255;
const MAX_ICC_PROFILE_SIZE: usize = MAX_CHUNK_SIZE * MAX_CHUNK_COUNT;

if self.icc_profile.len() > MAX_ICC_PROFILE_SIZE {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"ICC profile too large",
));
}

let chunk_iter = self.icc_profile.chunks(MAX_CHUNK_SIZE);
let num_chunks = chunk_iter.len() as u8;
let mut segment = Vec::new();

for (i, chunk) in chunk_iter.enumerate() {
let chunk_number = (i + 1) as u8;
let length = 14 + chunk.len();

segment.clear();
segment.reserve(length);
segment.extend_from_slice(b"ICC_PROFILE\0");
segment.push(chunk_number);
segment.push(num_chunks);
segment.extend_from_slice(chunk);

self.writer.write_segment(APP2, &segment)?;
}

Ok(())
}
}

impl<W: Write> ImageEncoder for JpegEncoder<W> {
Expand All @@ -661,6 +706,11 @@ impl<W: Write> ImageEncoder for JpegEncoder<W> {
) -> ImageResult<()> {
self.encode(buf, width, height, color_type)
}

fn set_icc_profile(&mut self, icc_profile: Vec<u8>) -> Result<(), UnsupportedError> {
self.icc_profile = icc_profile;
Ok(())
}
}

fn build_jfif_header(m: &mut Vec<u8>, density: PixelDensity) {
Expand Down
19 changes: 18 additions & 1 deletion src/codecs/png.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! * <http://www.w3.org/TR/PNG/> - The PNG Specification
//!

use std::borrow::Cow;
use std::fmt;
use std::io::{BufRead, Seek, Write};

Expand Down Expand Up @@ -482,6 +483,7 @@ pub struct PngEncoder<W: Write> {
w: W,
compression: CompressionType,
filter: FilterType,
icc_profile: Vec<u8>,
}

/// Compression level of a PNG encoder. The default setting is `Fast`.
Expand Down Expand Up @@ -535,6 +537,7 @@ impl<W: Write> PngEncoder<W> {
w,
compression: CompressionType::default(),
filter: FilterType::default(),
icc_profile: Vec::new(),
}
}

Expand All @@ -559,6 +562,7 @@ impl<W: Write> PngEncoder<W> {
w,
compression,
filter,
icc_profile: Vec::new(),
}
}

Expand Down Expand Up @@ -604,7 +608,15 @@ impl<W: Write> PngEncoder<W> {
FilterType::Adaptive => (png::FilterType::Sub, png::AdaptiveFilterType::Adaptive),
};

let mut encoder = png::Encoder::new(self.w, width, height);
let mut info = png::Info::with_size(width, height);

if !self.icc_profile.is_empty() {
info.icc_profile = Some(Cow::Borrowed(&self.icc_profile));
}

let mut encoder =
png::Encoder::with_info(self.w, info).map_err(|e| ImageError::IoError(e.into()))?;

encoder.set_color(ct);
encoder.set_depth(bits);
encoder.set_compression(comp);
Expand Down Expand Up @@ -669,6 +681,11 @@ impl<W: Write> ImageEncoder for PngEncoder<W> {
))),
}
}

fn set_icc_profile(&mut self, icc_profile: Vec<u8>) -> Result<(), UnsupportedError> {
self.icc_profile = icc_profile;
kornelski marked this conversation as resolved.
Show resolved Hide resolved
Ok(())
}
}

impl ImageError {
Expand Down
Loading