mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 00:46:57 -06:00
Create from images (doc_create, and image_xobject for stamps, signatures and inserted images) dropped a JPEG's or PNG's embedded ICC profile, so a CMYK JPEG became untagged /DeviceCMYK. - JPEG: the APP2 ICC_PROFILE chunks are joined by sequence number; a profile with a missing or repeated chunk is ignored. - PNG: the iCCP profile is read. - A profile whose header is sound (acsp) and whose colour space matches the image (GRAY/RGB/CMYK for 1/3/4 components) becomes [/ICCBased <stream>] with /N and the device space as /Alternate. Anything else keeps the device space, as before. JPEG data and an Adobe CMYK /Decode are unchanged. - Images in one document that carry the same profile share one profile object. TIFF profiles are not read yet. Co-authored-by: lloyd <lloyd@stackworx.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
lloyd
Claude Opus 5.5
parent
d5b5687f79
commit
06a7b9a7ca
@@ -6,7 +6,9 @@ Layer L4. Create a PDF (Acrobat's Create a PDF tool), execution plan M10.2:
|
||||
- `from_images(&[(name, bytes)])`: one page per image, sized from the image's resolution
|
||||
(PNG `pHYs`, JPEG JFIF density; 72 dpi when absent). JPEG data is embedded as is
|
||||
(`/DCTDecode`, grey, RGB or Adobe-inverted CMYK); PNG is decoded and stored with Flate,
|
||||
with transparency as a soft mask;
|
||||
with transparency as a soft mask. An ICC profile in the file (JPEG `APP2 ICC_PROFILE`, PNG
|
||||
`iCCP`) that matches the image's colour space is kept as an `/ICCBased` colour space with
|
||||
the device space as `/Alternate`; images with the same profile share one profile object;
|
||||
- `from_images_with_resolution(images, ImageResolution::Dpi(dpi))`: override the page
|
||||
resolution (1–1200 dpi), without resampling or recompressing image pixels. Use 72 dpi
|
||||
for one point per pixel, or `ImageResolution::Embedded` for the same behavior as
|
||||
|
||||
+84
-32
@@ -72,17 +72,79 @@ pub fn blank(width: f64, height: f64, pages: usize) -> Result<Document, CreateEr
|
||||
|
||||
// ── images ──────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// An image ready to embed: its XObject dictionary, encoded data, optional soft mask, and size
|
||||
/// in pixels and dots per inch.
|
||||
/// An image ready to embed: its XObject dictionary, encoded data, optional soft mask, the ICC
|
||||
/// profile the file carried (checked against the colour space by `icc_for`), and size in pixels
|
||||
/// and dots per inch.
|
||||
struct Embedded {
|
||||
dict: Dict,
|
||||
data: Vec<u8>,
|
||||
filtered: bool,
|
||||
smask: Option<(Dict, Vec<u8>)>,
|
||||
icc: Option<Vec<u8>>,
|
||||
px: (u32, u32),
|
||||
dpi: (f64, f64),
|
||||
}
|
||||
|
||||
/// `profile` when it can tag an image of `n` colour components: an ICC header (`acsp`) whose
|
||||
/// colour space is grey, RGB or CMYK to match. Anything else is left out, and the image keeps
|
||||
/// its device colour space.
|
||||
fn icc_for(profile: Vec<u8>, n: usize) -> Option<Vec<u8>> {
|
||||
let space: &[u8] = match n {
|
||||
1 => b"GRAY",
|
||||
3 => b"RGB ",
|
||||
4 => b"CMYK",
|
||||
_ => return None,
|
||||
};
|
||||
(profile.len() >= 128 && profile.get(36..40) == Some(b"acsp") && profile.get(16..20) == Some(space)).then_some(profile)
|
||||
}
|
||||
|
||||
/// A JPEG's ICC profile from its APP2 `ICC_PROFILE` chunks: (sequence number, chunk count,
|
||||
/// data). `None` unless every chunk from 1 to the count is there exactly once.
|
||||
fn jpeg_icc(mut chunks: Vec<(u8, u8, &[u8])>) -> Option<Vec<u8>> {
|
||||
let count = chunks.first()?.1;
|
||||
chunks.sort_by_key(|c| c.0);
|
||||
let complete = usize::from(count) == chunks.len() && chunks.iter().zip(1..=count).all(|(c, seq)| c.0 == seq && c.1 == count);
|
||||
complete.then(|| chunks.iter().flat_map(|c| c.2.iter().copied()).collect())
|
||||
}
|
||||
|
||||
/// Add `img` to `doc` as an image XObject. An ICC profile becomes an `/ICCBased` colour space
|
||||
/// (with the device space as `/Alternate`); `profiles` shares one object between images that
|
||||
/// carry the same profile.
|
||||
fn add_image(doc: &mut Document, img: Embedded, profiles: &mut Vec<(Vec<u8>, ObjRef)>) -> ObjRef {
|
||||
let mut d = img.dict;
|
||||
d.set(b"Type".to_vec(), Object::name("XObject"));
|
||||
d.set(b"Subtype".to_vec(), Object::name("Image"));
|
||||
d.set(b"Width".to_vec(), Object::Int(img.px.0 as i64));
|
||||
d.set(b"Height".to_vec(), Object::Int(img.px.1 as i64));
|
||||
let device = d.name(b"ColorSpace").map(<[u8]>::to_vec);
|
||||
let n = match device.as_deref() {
|
||||
Some(b"DeviceGray") => 1,
|
||||
Some(b"DeviceRGB") => 3,
|
||||
Some(b"DeviceCMYK") => 4,
|
||||
_ => 0,
|
||||
};
|
||||
if let (Some(icc), Some(device)) = (img.icc.and_then(|p| icc_for(p, n)), device) {
|
||||
let r = match profiles.iter().find(|(p, _)| *p == icc) {
|
||||
Some((_, r)) => *r,
|
||||
None => {
|
||||
let mut pd = Dict::new();
|
||||
pd.set(b"N".to_vec(), Object::Int(n as i64));
|
||||
pd.set(b"Alternate".to_vec(), Object::Name(device));
|
||||
let r = doc.add(Object::Stream(Stream::flate(pd, &icc)));
|
||||
profiles.push((icc, r));
|
||||
r
|
||||
}
|
||||
};
|
||||
d.set(b"ColorSpace".to_vec(), Object::Array(vec![Object::name("ICCBased"), Object::Ref(r)]));
|
||||
}
|
||||
if let Some((m, alpha)) = img.smask {
|
||||
let mr = doc.add(Object::Stream(Stream::flate(m, &alpha)));
|
||||
d.set(b"SMask".to_vec(), Object::Ref(mr));
|
||||
}
|
||||
let stream = if img.filtered { Stream::from_raw(d, img.data) } else { Stream::flate(d, &img.data) };
|
||||
doc.add(Object::Stream(stream))
|
||||
}
|
||||
|
||||
const JP2_SIGNATURE: &[u8] = &[0, 0, 0, 0x0C, b'j', b'P', b' ', b' ', 0x0D, 0x0A, 0x87, 0x0A];
|
||||
|
||||
fn be32(b: &[u8], at: usize) -> Option<u32> {
|
||||
@@ -156,7 +218,7 @@ fn jpx(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
|
||||
}
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Filter".to_vec(), Object::name("JPXDecode"));
|
||||
Ok(Embedded { dict: d, data: bytes.to_vec(), filtered: true, smask: None, px: (w, h), dpi })
|
||||
Ok(Embedded { dict: d, data: bytes.to_vec(), filtered: true, smask: None, icc: None, px: (w, h), dpi })
|
||||
}
|
||||
|
||||
fn jpeg(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
|
||||
@@ -165,6 +227,7 @@ fn jpeg(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
|
||||
return Err(bad("not a JPEG file"));
|
||||
}
|
||||
let (mut i, mut size, mut comps, mut dpi, mut adobe) = (2usize, None, 0u8, (72.0, 72.0), false);
|
||||
let mut icc_chunks = Vec::new();
|
||||
while i + 4 <= bytes.len() {
|
||||
if bytes[i] != 0xFF {
|
||||
i += 1;
|
||||
@@ -190,6 +253,12 @@ fn jpeg(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
|
||||
}
|
||||
}
|
||||
0xEE if seg.starts_with(b"Adobe") => adobe = true,
|
||||
// APP2 ICC profile, possibly split over several chunks.
|
||||
0xE2 => {
|
||||
if let Some(([seq, count], data)) = seg.strip_prefix(b"ICC_PROFILE\0").and_then(|r| r.split_first_chunk::<2>()) {
|
||||
icc_chunks.push((*seq, *count, data));
|
||||
}
|
||||
}
|
||||
0xC0..=0xCF if marker != 0xC4 && marker != 0xC8 && marker != 0xCC => {
|
||||
if seg.len() < 6 {
|
||||
return Err(bad("bad frame header"));
|
||||
@@ -220,7 +289,8 @@ fn jpeg(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
|
||||
d.set(b"ColorSpace".to_vec(), Object::name(cs));
|
||||
d.set(b"BitsPerComponent".to_vec(), Object::Int(8));
|
||||
d.set(b"Filter".to_vec(), Object::name("DCTDecode"));
|
||||
Ok(Embedded { dict: d, data: bytes.to_vec(), filtered: true, smask: None, px: (w, h), dpi })
|
||||
let icc = jpeg_icc(icc_chunks);
|
||||
Ok(Embedded { dict: d, data: bytes.to_vec(), filtered: true, smask: None, icc, px: (w, h), dpi })
|
||||
}
|
||||
|
||||
fn png_image(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
|
||||
@@ -232,6 +302,7 @@ fn png_image(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
|
||||
Some(png::PixelDimensions { xppu, yppu, unit: png::Unit::Meter }) if xppu > 0 && yppu > 0 => (xppu as f64 * 0.0254, yppu as f64 * 0.0254),
|
||||
_ => (72.0, 72.0),
|
||||
};
|
||||
let icc = reader.info().icc_profile.as_ref().map(|p| p.to_vec());
|
||||
let mut buf = vec![0; reader.output_buffer_size().ok_or_else(|| bad("image too large".into()))?];
|
||||
let frame = reader.next_frame(&mut buf).map_err(|e| bad(e.to_string()))?;
|
||||
let (w, h) = (frame.width, frame.height);
|
||||
@@ -266,7 +337,7 @@ fn png_image(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
|
||||
m.set(b"BitsPerComponent".to_vec(), Object::Int(8));
|
||||
(m, alpha)
|
||||
});
|
||||
Ok(Embedded { dict: d, data: colour, filtered: false, smask, px: (w, h), dpi })
|
||||
Ok(Embedded { dict: d, data: colour, filtered: false, smask, icc, px: (w, h), dpi })
|
||||
}
|
||||
|
||||
/// 8-bit RGBA pixels → an image (gray when every pixel is, with a soft mask when any pixel is
|
||||
@@ -292,7 +363,7 @@ fn rgba_image(rgba: &[u8], (w, h): (u32, u32), dpi: (f64, f64)) -> Embedded {
|
||||
m.set(b"BitsPerComponent".to_vec(), Object::Int(8));
|
||||
(m, rgba.as_chunks::<4>().0.iter().map(|p| p[3]).collect())
|
||||
});
|
||||
Embedded { dict: d, data: colour, filtered: false, smask, px: (w, h), dpi }
|
||||
Embedded { dict: d, data: colour, filtered: false, smask, icc: None, px: (w, h), dpi }
|
||||
}
|
||||
|
||||
/// BMP and GIF (the first frame), through the `image` decoders. Their resolution is not read:
|
||||
@@ -346,25 +417,25 @@ fn tiff_pages(name: &str, bytes: &[u8]) -> Result<Vec<Embedded>, CreateError> {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"ColorSpace".to_vec(), Object::name("DeviceGray"));
|
||||
d.set(b"BitsPerComponent".to_vec(), Object::Int(1));
|
||||
Embedded { dict: d, data, filtered: false, smask: None, px: (w, h), dpi }
|
||||
Embedded { dict: d, data, filtered: false, smask: None, icc: None, px: (w, h), dpi }
|
||||
}
|
||||
C::Gray(8 | 16) => {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"ColorSpace".to_vec(), Object::name("DeviceGray"));
|
||||
d.set(b"BitsPerComponent".to_vec(), Object::Int(8));
|
||||
Embedded { dict: d, data, filtered: false, smask: None, px: (w, h), dpi }
|
||||
Embedded { dict: d, data, filtered: false, smask: None, icc: None, px: (w, h), dpi }
|
||||
}
|
||||
C::RGB(8 | 16) => {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"ColorSpace".to_vec(), Object::name("DeviceRGB"));
|
||||
d.set(b"BitsPerComponent".to_vec(), Object::Int(8));
|
||||
Embedded { dict: d, data, filtered: false, smask: None, px: (w, h), dpi }
|
||||
Embedded { dict: d, data, filtered: false, smask: None, icc: None, px: (w, h), dpi }
|
||||
}
|
||||
C::CMYK(8 | 16) => {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"ColorSpace".to_vec(), Object::name("DeviceCMYK"));
|
||||
d.set(b"BitsPerComponent".to_vec(), Object::Int(8));
|
||||
Embedded { dict: d, data, filtered: false, smask: None, px: (w, h), dpi }
|
||||
Embedded { dict: d, data, filtered: false, smask: None, icc: None, px: (w, h), dpi }
|
||||
}
|
||||
C::RGBA(8 | 16) => rgba_image(&data, (w, h), dpi),
|
||||
C::GrayA(8 | 16) => {
|
||||
@@ -445,17 +516,7 @@ fn embed(name: &str, bytes: &[u8]) -> Result<Vec<Embedded>, CreateError> {
|
||||
pub fn image_xobject(doc: &mut Document, name: &str, bytes: &[u8]) -> Result<(ObjRef, (f64, f64)), CreateError> {
|
||||
let img = embed(name, bytes)?.into_iter().next().ok_or_else(|| CreateError::Image(name.into(), "the file has no image".into()))?;
|
||||
let size = (img.px.0 as f64 * 72.0 / img.dpi.0, img.px.1 as f64 * 72.0 / img.dpi.1);
|
||||
let mut d = img.dict;
|
||||
d.set(b"Type".to_vec(), Object::name("XObject"));
|
||||
d.set(b"Subtype".to_vec(), Object::name("Image"));
|
||||
d.set(b"Width".to_vec(), Object::Int(img.px.0 as i64));
|
||||
d.set(b"Height".to_vec(), Object::Int(img.px.1 as i64));
|
||||
if let Some((m, alpha)) = img.smask {
|
||||
let mr = doc.add(Object::Stream(Stream::flate(m, &alpha)));
|
||||
d.set(b"SMask".to_vec(), Object::Ref(mr));
|
||||
}
|
||||
let stream = if img.filtered { Stream::from_raw(d, img.data) } else { Stream::flate(d, &img.data) };
|
||||
Ok((doc.add(Object::Stream(stream)), size))
|
||||
Ok((add_image(doc, img, &mut Vec::new()), size))
|
||||
}
|
||||
|
||||
/// Resolution used to size PDF pages. Image pixels are never resampled.
|
||||
@@ -484,6 +545,7 @@ pub fn from_images_with_resolution(images: &[(String, Vec<u8>)], resolution: Ima
|
||||
return Err(CreateError::Invalid("no images".into()));
|
||||
}
|
||||
let mut doc = Document::new_empty();
|
||||
let mut profiles = Vec::new();
|
||||
for img in images.iter().map(|(name, bytes)| embed(name, bytes)).collect::<Result<Vec<_>, _>>()?.into_iter().flatten() {
|
||||
let dpi = match resolution {
|
||||
ImageResolution::Embedded => img.dpi,
|
||||
@@ -494,17 +556,7 @@ pub fn from_images_with_resolution(images: &[(String, Vec<u8>)], resolution: Ima
|
||||
let k = (MAX_SIDE / w.max(h)).min(1.0);
|
||||
w *= k;
|
||||
h *= k;
|
||||
let mut d = img.dict;
|
||||
d.set(b"Type".to_vec(), Object::name("XObject"));
|
||||
d.set(b"Subtype".to_vec(), Object::name("Image"));
|
||||
d.set(b"Width".to_vec(), Object::Int(img.px.0 as i64));
|
||||
d.set(b"Height".to_vec(), Object::Int(img.px.1 as i64));
|
||||
if let Some((m, alpha)) = img.smask {
|
||||
let mr = doc.add(Object::Stream(Stream::flate(m, &alpha)));
|
||||
d.set(b"SMask".to_vec(), Object::Ref(mr));
|
||||
}
|
||||
let stream = if img.filtered { Stream::from_raw(d, img.data) } else { Stream::flate(d, &img.data) };
|
||||
let xr = doc.add(Object::Stream(stream));
|
||||
let xr = add_image(&mut doc, img, &mut profiles);
|
||||
let mut xobj = Dict::new();
|
||||
xobj.set(b"Im0".to_vec(), Object::Ref(xr));
|
||||
let mut res = Dict::new();
|
||||
|
||||
@@ -303,3 +303,103 @@ fn source_kind_tells_pdfs_images_and_text_apart() {
|
||||
assert_eq!(source_kind("a.docx", b"PK\x03\x04"), None);
|
||||
assert_eq!(source_kind("", b""), None);
|
||||
}
|
||||
|
||||
/// A synthetic ICC profile: a 128-byte header (size, device class, colour space, `acsp`) and
|
||||
/// some payload. Only the header is read.
|
||||
fn icc_profile(space: &[u8; 4]) -> Vec<u8> {
|
||||
let mut p = vec![0u8; 300];
|
||||
p[..4].copy_from_slice(&300u32.to_be_bytes());
|
||||
p[12..16].copy_from_slice(b"prtr");
|
||||
p[16..20].copy_from_slice(space);
|
||||
p[36..40].copy_from_slice(b"acsp");
|
||||
for (i, b) in p.iter_mut().enumerate().skip(128) {
|
||||
*b = i as u8;
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// A minimal CMYK JPEG header (SOI, Adobe APP14, `profile` in two APP2 ICC_PROFILE chunks,
|
||||
/// SOF0 3×2 with four components, EOI).
|
||||
fn cmyk_jpeg_bytes(profile: &[u8]) -> Vec<u8> {
|
||||
let mut v = vec![0xFF, 0xD8];
|
||||
v.extend_from_slice(&[0xFF, 0xEE, 0, 14, b'A', b'd', b'o', b'b', b'e', 0, 100, 0, 0, 0, 0, 2]);
|
||||
let (a, b) = profile.split_at(profile.len() / 2);
|
||||
// Written out of order: the sequence numbers decide.
|
||||
for (seq, part) in [(2u8, b), (1, a)] {
|
||||
v.extend_from_slice(&[0xFF, 0xE2]);
|
||||
v.extend_from_slice(&(2 + 14 + part.len() as u16).to_be_bytes());
|
||||
v.extend_from_slice(b"ICC_PROFILE\0");
|
||||
v.extend_from_slice(&[seq, 2]);
|
||||
v.extend_from_slice(part);
|
||||
}
|
||||
v.extend_from_slice(&[0xFF, 0xC0, 0, 20, 8, 0, 2, 0, 3, 4, 1, 0x11, 0, 2, 0x11, 0, 3, 0x11, 0, 4, 0x11, 0]);
|
||||
v.extend_from_slice(&[0xFF, 0xD9]);
|
||||
v
|
||||
}
|
||||
|
||||
/// The ICC profile behind an image's colour space: (`/N`, `/Alternate`, profile bytes, object).
|
||||
fn icc_of(doc: &Document, image: &Dict) -> Option<(i64, Vec<u8>, Vec<u8>, Object)> {
|
||||
let cs = image.get(b"ColorSpace")?.as_array()?.clone();
|
||||
assert_eq!(cs.first().and_then(Object::as_name), Some(&b"ICCBased"[..]));
|
||||
let r = cs.get(1)?.clone();
|
||||
let Object::Stream(s) = &*doc.resolve(&r) else { panic!("the ICC profile is not a stream") };
|
||||
Some((s.dict.int(b"N")?, s.dict.name(b"Alternate")?.to_vec(), s.decoded().unwrap(), r))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_icc_profiles_tag_the_image_colour_space() {
|
||||
// A CMYK JPEG keeps its profile as /ICCBased (N 4), its data byte for byte and its Decode.
|
||||
let cmyk = icc_profile(b"CMYK");
|
||||
let jpeg = cmyk_jpeg_bytes(&cmyk);
|
||||
let rgb = icc_profile(b"RGB ");
|
||||
let png = {
|
||||
let mut out = Vec::new();
|
||||
let mut info = png::Info::with_size(2, 1);
|
||||
info.color_type = png::ColorType::Rgb;
|
||||
info.bit_depth = png::BitDepth::Eight;
|
||||
info.icc_profile = Some(rgb.clone().into());
|
||||
let mut w = png::Encoder::with_info(&mut out, info).unwrap().write_header().unwrap();
|
||||
w.write_image_data(&[10, 20, 30, 40, 50, 60]).unwrap();
|
||||
w.finish().unwrap();
|
||||
out
|
||||
};
|
||||
let doc = reopen(&from_images(&[("a.jpg".into(), jpeg.clone()), ("b.jpg".into(), jpeg.clone()), ("c.png".into(), png)]).unwrap());
|
||||
let (n, alt, data, first) = icc_of(&doc, &image_of(&doc, 0)).expect("the CMYK JPEG is ICC-tagged");
|
||||
assert_eq!((n, alt.as_slice(), data), (4, &b"DeviceCMYK"[..], cmyk.clone()));
|
||||
assert!(image_of(&doc, 0).contains(b"Decode"), "Adobe CMYK stays inverted");
|
||||
let p = &pages(&doc)[0];
|
||||
let res = doc.resolve(p.get(b"Resources").unwrap()).as_dict().cloned().unwrap();
|
||||
let xo = doc.resolve(res.get(b"XObject").unwrap()).as_dict().cloned().unwrap();
|
||||
let Object::Stream(s) = &*doc.resolve(xo.get(b"Im0").unwrap()) else { panic!() };
|
||||
assert_eq!(*s.raw, jpeg, "the JPEG data is embedded as is");
|
||||
let (_, _, _, second) = icc_of(&doc, &image_of(&doc, 1)).unwrap();
|
||||
assert_eq!(first, second, "images with the same profile share one profile object");
|
||||
// A PNG iCCP profile tags an RGB image (N 3).
|
||||
let (n, alt, data, _) = icc_of(&doc, &image_of(&doc, 2)).expect("the PNG is ICC-tagged");
|
||||
assert_eq!((n, alt.as_slice(), data), (3, &b"DeviceRGB"[..], rgb.clone()));
|
||||
// The colour space is still exported as CMYK / RGB.
|
||||
let out = extract_images(&doc, &[0, 1, 2], 0);
|
||||
assert_eq!(out.images.len(), 3, "{:?}", out.skipped);
|
||||
|
||||
// A profile for another colour space, a damaged one or a missing chunk: the device space.
|
||||
let mut no_acsp = cmyk.clone();
|
||||
no_acsp[36] = b'x';
|
||||
let missing_chunk = {
|
||||
let mut v = cmyk_jpeg_bytes(&cmyk);
|
||||
// Drop the first APP2 segment (sequence 2 of 2).
|
||||
let at = v.windows(2).position(|w| w == [0xFF, 0xE2]).unwrap();
|
||||
let len = u16::from_be_bytes([v[at + 2], v[at + 3]]) as usize;
|
||||
v.drain(at..at + 2 + len);
|
||||
v
|
||||
};
|
||||
for (what, bytes) in [("an RGB profile", cmyk_jpeg_bytes(&rgb)), ("no acsp", cmyk_jpeg_bytes(&no_acsp)), ("a missing chunk", missing_chunk)] {
|
||||
let doc = reopen(&from_images(&[("x.jpg".into(), bytes)]).unwrap());
|
||||
assert_eq!(image_of(&doc, 0).name(b"ColorSpace"), Some(&b"DeviceCMYK"[..]), "{what}");
|
||||
}
|
||||
|
||||
// image_xobject (stamps, signatures, inserted images) tags its image the same way.
|
||||
let mut doc = Document::new_empty();
|
||||
let (r, _) = image_xobject(&mut doc, "a.jpg", &jpeg).unwrap();
|
||||
let Object::Stream(s) = &*doc.get(r) else { panic!() };
|
||||
assert_eq!(icc_of(&doc, &s.dict).map(|(n, ..)| n), Some(4));
|
||||
}
|
||||
|
||||
@@ -6696,8 +6696,8 @@ milestone = "M10"
|
||||
status = "shipped"
|
||||
commands = ["create.images"]
|
||||
tools = ["doc_create"]
|
||||
evidence = ["crates/create/src/tests.rs::jpeg_2000_images_are_embedded_as_is", "crates/create/src/tests.rs::images_export_as_jpeg_unchanged_and_others_as_png"]
|
||||
notes = "PNG, JPEG, TIFF (multi-page), GIF (first frame), BMP, and JPEG 2000 (JP2 or codestream, embedded as JPXDecode with its capture resolution)"
|
||||
evidence = ["crates/create/src/tests.rs::jpeg_2000_images_are_embedded_as_is", "crates/create/src/tests.rs::images_export_as_jpeg_unchanged_and_others_as_png", "crates/create/src/tests.rs::embedded_icc_profiles_tag_the_image_colour_space"]
|
||||
notes = "PNG, JPEG, TIFF (multi-page), GIF (first frame), BMP, and JPEG 2000 (JP2 or codestream, embedded as JPXDecode with its capture resolution). JPEG and PNG ICC profiles are kept as /ICCBased; TIFF profiles are not read yet"
|
||||
|
||||
[[feature]]
|
||||
id = "create.from-multipage-tiff"
|
||||
|
||||
Reference in New Issue
Block a user