Skip to content

Commit 2125965

Browse files
authored
Merge pull request #2389 from alula/alula/jpeg-png-icc-profile
Implement writing ICC profiles for JPEG and PNG images.
2 parents 5414013 + 472e3fb commit 2125965

File tree

3 files changed

+71
-4
lines changed

3 files changed

+71
-4
lines changed

Cargo.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ num-traits = { version = "0.2.0" }
4343
color_quant = { version = "1.1", optional = true }
4444
dav1d = { version = "0.10.3", optional = true }
4545
exr = { version = "1.5.0", optional = true }
46-
gif = { version = "0.13", optional = true }
46+
gif = { version = "0.13.1", optional = true }
4747
image-webp = { version = "0.2.0", optional = true }
4848
mp4parse = { version = "0.17.0", optional = true }
49-
png = { version = "0.17.6", optional = true }
49+
png = { version = "0.17.11", optional = true }
5050
qoi = { version = "0.4", optional = true }
5151
ravif = { version = "0.11.11", default-features = false, optional = true }
5252
rayon = { version = "1.7.0", optional = true }
@@ -114,4 +114,4 @@ harness = false
114114
[[bench]]
115115
path = "benches/blur.rs"
116116
name = "blur"
117-
harness = false
117+
harness = false

src/codecs/jpeg/encoder.rs

+50
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ static SOS: u8 = 0xDA;
3030
static DQT: u8 = 0xDB;
3131
// Application segments start and end
3232
static APP0: u8 = 0xE0;
33+
static APP2: u8 = 0xE2;
3334

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

348349
pixel_density: PixelDensity,
350+
351+
icc_profile: Vec<u8>,
349352
}
350353

351354
impl<W: Write> JpegEncoder<W> {
@@ -415,6 +418,8 @@ impl<W: Write> JpegEncoder<W> {
415418
chroma_actable: Cow::Borrowed(&STD_CHROMA_AC_HUFF_LUT),
416419

417420
pixel_density: PixelDensity::default(),
421+
422+
icc_profile: Vec::new(),
418423
}
419424
}
420425

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

502+
// Write ICC profile chunks if present
503+
self.write_icc_profile_chunks()?;
504+
497505
build_frame_header(
498506
&mut buf,
499507
8,
@@ -648,6 +656,43 @@ impl<W: Write> JpegEncoder<W> {
648656

649657
Ok(())
650658
}
659+
660+
fn write_icc_profile_chunks(&mut self) -> io::Result<()> {
661+
if self.icc_profile.is_empty() {
662+
return Ok(());
663+
}
664+
665+
const MAX_CHUNK_SIZE: usize = 65533 - 14;
666+
const MAX_CHUNK_COUNT: usize = 255;
667+
const MAX_ICC_PROFILE_SIZE: usize = MAX_CHUNK_SIZE * MAX_CHUNK_COUNT;
668+
669+
if self.icc_profile.len() > MAX_ICC_PROFILE_SIZE {
670+
return Err(io::Error::new(
671+
io::ErrorKind::InvalidInput,
672+
"ICC profile too large",
673+
));
674+
}
675+
676+
let chunk_iter = self.icc_profile.chunks(MAX_CHUNK_SIZE);
677+
let num_chunks = chunk_iter.len() as u8;
678+
let mut segment = Vec::new();
679+
680+
for (i, chunk) in chunk_iter.enumerate() {
681+
let chunk_number = (i + 1) as u8;
682+
let length = 14 + chunk.len();
683+
684+
segment.clear();
685+
segment.reserve(length);
686+
segment.extend_from_slice(b"ICC_PROFILE\0");
687+
segment.push(chunk_number);
688+
segment.push(num_chunks);
689+
segment.extend_from_slice(chunk);
690+
691+
self.writer.write_segment(APP2, &segment)?;
692+
}
693+
694+
Ok(())
695+
}
651696
}
652697

653698
impl<W: Write> ImageEncoder for JpegEncoder<W> {
@@ -661,6 +706,11 @@ impl<W: Write> ImageEncoder for JpegEncoder<W> {
661706
) -> ImageResult<()> {
662707
self.encode(buf, width, height, color_type)
663708
}
709+
710+
fn set_icc_profile(&mut self, icc_profile: Vec<u8>) -> Result<(), UnsupportedError> {
711+
self.icc_profile = icc_profile;
712+
Ok(())
713+
}
664714
}
665715

666716
fn build_jfif_header(m: &mut Vec<u8>, density: PixelDensity) {

src/codecs/png.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//! * <http://www.w3.org/TR/PNG/> - The PNG Specification
77
//!
88
9+
use std::borrow::Cow;
910
use std::fmt;
1011
use std::io::{BufRead, Seek, Write};
1112

@@ -482,6 +483,7 @@ pub struct PngEncoder<W: Write> {
482483
w: W,
483484
compression: CompressionType,
484485
filter: FilterType,
486+
icc_profile: Vec<u8>,
485487
}
486488

487489
/// Compression level of a PNG encoder. The default setting is `Fast`.
@@ -535,6 +537,7 @@ impl<W: Write> PngEncoder<W> {
535537
w,
536538
compression: CompressionType::default(),
537539
filter: FilterType::default(),
540+
icc_profile: Vec::new(),
538541
}
539542
}
540543

@@ -559,6 +562,7 @@ impl<W: Write> PngEncoder<W> {
559562
w,
560563
compression,
561564
filter,
565+
icc_profile: Vec::new(),
562566
}
563567
}
564568

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

607-
let mut encoder = png::Encoder::new(self.w, width, height);
611+
let mut info = png::Info::with_size(width, height);
612+
613+
if !self.icc_profile.is_empty() {
614+
info.icc_profile = Some(Cow::Borrowed(&self.icc_profile));
615+
}
616+
617+
let mut encoder =
618+
png::Encoder::with_info(self.w, info).map_err(|e| ImageError::IoError(e.into()))?;
619+
608620
encoder.set_color(ct);
609621
encoder.set_depth(bits);
610622
encoder.set_compression(comp);
@@ -669,6 +681,11 @@ impl<W: Write> ImageEncoder for PngEncoder<W> {
669681
))),
670682
}
671683
}
684+
685+
fn set_icc_profile(&mut self, icc_profile: Vec<u8>) -> Result<(), UnsupportedError> {
686+
self.icc_profile = icc_profile;
687+
Ok(())
688+
}
672689
}
673690

674691
impl ImageError {

0 commit comments

Comments
 (0)