mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 07:10:13 +00:00
PDF export: a malformed gray or RGB profile is left out of an image
A gray or RGB image is tagged with the ICC profile it carries only when the profile passes the checks a CMYK image's profile already passes: at most 16 MiB, the size its header declares, the acsp signature, an input, display, output or color space profile class, and a tag table that lies within it. Otherwise the image is written in DeviceGray or DeviceRGB, kept as it is, resampled or compressed with JPEG.
This commit is contained in:
+14
-14
@@ -68,15 +68,15 @@ impl CustomImage for Raw {
|
||||
}
|
||||
}
|
||||
|
||||
/// `icc` when it is a profile of the colour space `space` names: `GRAY` for grey samples, `RGB `
|
||||
/// for RGB ones. `None` for other profiles and spaces.
|
||||
/// `icc` when it is a well-formed profile ([`valid_icc`]) of the colour space `space` names: `GRAY`
|
||||
/// for grey samples, `RGB ` for RGB ones. `None` for other profiles and spaces.
|
||||
fn icc_for(icc: Option<Vec<u8>>, space: ImageColorspace) -> Option<Arc<Vec<u8>>> {
|
||||
let want = match space {
|
||||
ImageColorspace::Luma => *b"GRAY",
|
||||
ImageColorspace::Rgb => *b"RGB ",
|
||||
ImageColorspace::Cmyk | ImageColorspace::DeviceN => return None,
|
||||
};
|
||||
icc.filter(|icc| vectorcraft_doc::cmyk::icc_color_space(icc) == Some(want)).map(Arc::new)
|
||||
icc.filter(|icc| valid_icc(icc, want)).map(Arc::new)
|
||||
}
|
||||
|
||||
/// The ICC profile image `bytes` carries: a TIFF's tag 34675 (read by the TIFF decoder itself,
|
||||
@@ -100,7 +100,7 @@ fn from_rgba(rgba: &[u8], width: u32, height: u32, interpolate: bool, pixels: Op
|
||||
if px.len() as u64 != width as u64 * height as u64 || px.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let rgb_icc = pixels.is_none() && icc.as_deref().and_then(vectorcraft_doc::cmyk::icc_color_space) == Some(*b"RGB ");
|
||||
let rgb_icc = pixels.is_none() && icc.as_deref().is_some_and(|icc| valid_icc(icc, *b"RGB "));
|
||||
let (color, space) = if neutral(rgba) && !rgb_icc {
|
||||
(px.iter().map(|p| p[0]).collect(), ImageColorspace::Luma)
|
||||
} else if let Some(cmyk) = pixels.and_then(|p| p.cmyk(rgba)) {
|
||||
@@ -175,19 +175,20 @@ fn lossless(
|
||||
/// it holds is still written.
|
||||
pub(crate) const MAX_EMBED_PIXELS: u64 = 1 << 27;
|
||||
|
||||
/// The most bytes a CMYK profile carried by an image is written with (16 MiB).
|
||||
/// The most bytes an ICC profile carried by an image is written with (16 MiB).
|
||||
const MAX_ICC: usize = 16 << 20;
|
||||
|
||||
/// The CMYK ICC profile image `blob` carries ([`ImageBlob::cmyk_icc_profile`]), when it is one a
|
||||
/// CMYK image can be tagged with: at most [`MAX_ICC`] bytes, the size its header declares, the
|
||||
/// `acsp` signature, an input, display, output or colour space profile of CMYK colours, and a tag
|
||||
/// table whose tags lie within it. `None` for other images and other profiles.
|
||||
/// CMYK image can be tagged with ([`valid_icc`]). `None` for other images and other profiles.
|
||||
pub(crate) fn cmyk_icc(blob: &ImageBlob) -> Option<Vec<u8>> {
|
||||
blob.cmyk_icc_profile().filter(|icc| valid_cmyk_icc(icc))
|
||||
blob.cmyk_icc_profile().filter(|icc| valid_icc(icc, *b"CMYK"))
|
||||
}
|
||||
|
||||
/// Is `icc` a CMYK profile as [`cmyk_icc`] describes?
|
||||
fn valid_cmyk_icc(icc: &[u8]) -> bool {
|
||||
/// Is `icc` a profile an image of colour space `space` (`GRAY`, `RGB ` or `CMYK`) can be tagged
|
||||
/// with: at most [`MAX_ICC`] bytes, the size its header declares, the `acsp` signature, an input,
|
||||
/// display, output or colour space profile of `space` colours, and a tag table whose tags lie
|
||||
/// within it?
|
||||
fn valid_icc(icc: &[u8], space: [u8; 4]) -> bool {
|
||||
let be = |at: usize| icc.get(at..at + 4).and_then(|b| b.try_into().ok()).map(u32::from_be_bytes);
|
||||
let fits = |at: u32, len: u32| u64::from(at) + u64::from(len) <= icc.len() as u64;
|
||||
if icc.len() > MAX_ICC || be(0).and_then(|n| usize::try_from(n).ok()) != Some(icc.len()) {
|
||||
@@ -195,7 +196,7 @@ fn valid_cmyk_icc(icc: &[u8]) -> bool {
|
||||
}
|
||||
let class = icc.get(12..16);
|
||||
if icc.get(36..40) != Some(b"acsp".as_slice())
|
||||
|| icc.get(16..20) != Some(b"CMYK".as_slice())
|
||||
|| icc.get(16..20) != Some(space.as_slice())
|
||||
|| ![b"scnr", b"mntr", b"prtr", b"spac"].iter().any(|c| class == Some(c.as_slice()))
|
||||
{
|
||||
return false;
|
||||
@@ -412,8 +413,7 @@ pub(crate) fn recode(
|
||||
// Pixels that keep their colours keep the profile they are in; converted ones are in the
|
||||
// destination's.
|
||||
let want = if kind == Kind::Color { *b"RGB " } else { *b"GRAY" };
|
||||
let icc =
|
||||
(profiles && convert.is_none()).then(|| source_icc(bytes)).flatten().filter(|icc| vectorcraft_doc::cmyk::icc_color_space(icc) == Some(want));
|
||||
let icc = (profiles && convert.is_none()).then(|| source_icc(bytes)).flatten().filter(|icc| valid_icc(icc, want));
|
||||
let rgb_icc = icc.is_some() && kind == Kind::Color;
|
||||
let mut rgba = resample(img, &rule, sized(&rule, dims, size)).into_rgba8();
|
||||
if kind == Kind::Mono {
|
||||
|
||||
@@ -270,8 +270,8 @@ fn images_declaring_more_than_the_allowance_are_not_decoded() {
|
||||
assert!(r.image.is_none(), "kept as it is");
|
||||
}
|
||||
|
||||
/// A version 2.1 display ICC profile header for colour space `space` (`RGB ` or `GRAY`), `len`
|
||||
/// bytes in all, every byte after the header different from its neighbors.
|
||||
/// A version 2.1 display ICC profile for colour space `space` (`RGB ` or `GRAY`), `len` bytes in
|
||||
/// all, with an empty tag table, every byte after the tag count different from its neighbors.
|
||||
fn icc_profile(space: &[u8; 4], len: usize) -> Vec<u8> {
|
||||
let mut icc: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
|
||||
icc[..4].copy_from_slice(&u32::try_from(len).unwrap().to_be_bytes());
|
||||
@@ -279,6 +279,7 @@ fn icc_profile(space: &[u8; 4], len: usize) -> Vec<u8> {
|
||||
icc[12..16].copy_from_slice(b"mntr");
|
||||
icc[16..20].copy_from_slice(space);
|
||||
icc[36..40].copy_from_slice(b"acsp");
|
||||
icc[128..132].copy_from_slice(&0u32.to_be_bytes());
|
||||
icc
|
||||
}
|
||||
|
||||
@@ -450,6 +451,24 @@ fn tagged_tiffs_keep_their_icc_profile() {
|
||||
// A profile of another colour space is left out.
|
||||
let spaces = export(&tiff(&colour(), &gray), json!({}));
|
||||
assert!(spaces.len() == 1 && spaces[0].space == "DeviceRGB" && spaces[0].icc.is_none(), "RGB in a gray profile: {spaces:?}");
|
||||
// A malformed profile is left out, the image kept as it is, resampled or compressed with JPEG:
|
||||
// one whose header declares another size, one without the `acsp` signature, and one whose tag
|
||||
// table runs past its end.
|
||||
let mut malformed = [rgb.clone(), rgb.clone(), rgb.clone()];
|
||||
malformed[0][..4].copy_from_slice(&601u32.to_be_bytes());
|
||||
malformed[1][36..40].copy_from_slice(b"xxxx");
|
||||
malformed[2][128..132].copy_from_slice(&100u32.to_be_bytes());
|
||||
let compressions =
|
||||
[json!({}), json!({"color": {"downsample": "bicubic", "ppi": 150, "abovePpi": 225}}), json!({"color": {"compression": "jpeg"}})];
|
||||
for (i, icc) in malformed.iter().enumerate() {
|
||||
for compression in &compressions {
|
||||
let spaces = export(&tiff(&colour(), icc), json!({ "compression": compression }));
|
||||
assert!(
|
||||
spaces.len() == 1 && spaces[0].space == "DeviceRGB" && spaces[0].icc.is_none(),
|
||||
"malformed profile {i}, {compression}: {spaces:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
// Converted pixels are in the destination's colours, not the image's profile.
|
||||
for destination in [vectorcraft_color::cms::DISPLAY_P3, vectorcraft_color::cms::GENERIC_CMYK] {
|
||||
let spaces = export(&tiff(&colour(), &rgb), json!({"output": {"conversion": "destination", "destination": destination}}));
|
||||
|
||||
Reference in New Issue
Block a user