mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Merge pull request #1442 from HEOJUNFO/fix/grey-tiff-device-gray
Export placed greyscale TIFFs as DeviceGray
This commit is contained in:
@@ -1629,6 +1629,21 @@ fn is_rgb_icc(icc: &[u8]) -> bool {
|
||||
icc.get(16..20) == Some(b"RGB ".as_slice())
|
||||
}
|
||||
|
||||
/// Whether an ICC profile describes grey.
|
||||
fn is_grey_icc(icc: &[u8]) -> bool {
|
||||
icc.get(16..20) == Some(b"GRAY".as_slice())
|
||||
}
|
||||
|
||||
/// Whether `data` decodes to grey (with or without alpha), read from its header.
|
||||
fn is_grey(data: &[u8]) -> bool {
|
||||
use image::ImageDecoder;
|
||||
image::ImageReader::new(std::io::Cursor::new(data))
|
||||
.with_guessed_format()
|
||||
.ok()
|
||||
.and_then(|r| r.into_decoder().ok())
|
||||
.is_some_and(|dec| !dec.color_type().has_color())
|
||||
}
|
||||
|
||||
/// A JPEG, passed through. With `tag_rgb`, a JPEG keeps its own profile, and one without gets
|
||||
/// sRGB. krilla uses a profile only when its channels match the JPEG's, so a grey or CMYK JPEG
|
||||
/// without a profile stays DeviceGray or DeviceCMYK.
|
||||
@@ -1668,6 +1683,10 @@ fn lossless(data: &Arc<Vec<u8>>, fmt: Option<image::ImageFormat>, mode: RasterMo
|
||||
_ => None,
|
||||
};
|
||||
direct.or_else(|| {
|
||||
// A grey TIFF, BMP… stays DeviceGray, as a grey PNG or JPEG does (#1426).
|
||||
if let Some(grey) = GreyImage::new(data) {
|
||||
return Image::from_custom(grey, i).ok();
|
||||
}
|
||||
// TIFF, BMP, PSD composites… and, with `tag_rgb`, untagged RGB.
|
||||
if mode.tag_rgb {
|
||||
return SrgbImage::new(rgba).and_then(|img| Image::from_custom(img, i).ok());
|
||||
@@ -1722,6 +1741,61 @@ impl krilla::image::CustomImage for SrgbImage {
|
||||
}
|
||||
}
|
||||
|
||||
/// A grey raster as a DeviceGray image (ICCBased when the file has a grey profile of its own), so
|
||||
/// a page that only holds black prints as black, not as RGB.
|
||||
#[derive(Clone, Hash)]
|
||||
struct GreyImage {
|
||||
grey: Arc<Vec<u8>>,
|
||||
/// Only when some pixel is not opaque.
|
||||
alpha: Option<Arc<Vec<u8>>>,
|
||||
size: (u32, u32),
|
||||
icc: Option<Arc<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl GreyImage {
|
||||
/// `None` when `data` isn't grey or can't be decoded.
|
||||
fn new(data: &[u8]) -> Option<Self> {
|
||||
if !is_grey(data) {
|
||||
return None;
|
||||
}
|
||||
let img = image::load_from_memory(data).ok()?.to_luma_alpha8();
|
||||
let size = img.dimensions();
|
||||
let pixels = usize::try_from(size.0).ok()?.checked_mul(usize::try_from(size.1).ok()?)?;
|
||||
if pixels == 0 {
|
||||
return None;
|
||||
}
|
||||
let (mut grey, mut alpha) = (Vec::with_capacity(pixels), Vec::with_capacity(pixels));
|
||||
for p in img.pixels() {
|
||||
grey.push(p.0[0]);
|
||||
alpha.push(p.0[1]);
|
||||
}
|
||||
let alpha = alpha.iter().any(|a| *a < 255).then(|| Arc::new(alpha));
|
||||
let icc = embedded_icc(data).filter(|p| is_grey_icc(p)).map(Arc::new);
|
||||
Some(Self { grey: Arc::new(grey), alpha, size, icc })
|
||||
}
|
||||
}
|
||||
|
||||
impl krilla::image::CustomImage for GreyImage {
|
||||
fn color_channel(&self) -> &[u8] {
|
||||
&self.grey
|
||||
}
|
||||
fn alpha_channel(&self) -> Option<&[u8]> {
|
||||
self.alpha.as_deref().map(Vec::as_slice)
|
||||
}
|
||||
fn bits_per_component(&self) -> krilla::image::BitsPerComponent {
|
||||
krilla::image::BitsPerComponent::Eight
|
||||
}
|
||||
fn size(&self) -> (u32, u32) {
|
||||
self.size
|
||||
}
|
||||
fn icc_profile(&self) -> Option<&[u8]> {
|
||||
self.icc.as_deref().map(Vec::as_slice)
|
||||
}
|
||||
fn color_space(&self) -> krilla::image::ImageColorspace {
|
||||
krilla::image::ImageColorspace::Luma
|
||||
}
|
||||
}
|
||||
|
||||
/// A CMYK raster as a krilla image: its samples are written unchanged as DeviceCMYK, like a
|
||||
/// CMYK JPEG's. An embedded ICC profile is left out, as it is for JPEGs: the inks are meant for
|
||||
/// the output intent's press, and tagging them with another CMYK profile would let a RIP
|
||||
@@ -1768,11 +1842,18 @@ fn recompress(data: &[u8], mode: RasterMode) -> Option<Image> {
|
||||
if img.color().has_alpha() && img.to_rgba8().pixels().any(|p| p[3] < 255) {
|
||||
return None;
|
||||
}
|
||||
let rgb = img.to_rgb8();
|
||||
let mut buf = Vec::new();
|
||||
let enc = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 90);
|
||||
image::ImageEncoder::write_image(enc, rgb.as_raw(), rgb.width(), rgb.height(), image::ExtendedColorType::Rgb8).ok()?;
|
||||
jpeg(buf.into(), || embedded_icc(data).filter(|p| is_rgb_icc(p)), mode)
|
||||
// A grey image stays a grey JPEG (#1426).
|
||||
let grey = !img.color().has_color();
|
||||
if grey {
|
||||
let l = img.to_luma8();
|
||||
image::ImageEncoder::write_image(enc, l.as_raw(), l.width(), l.height(), image::ExtendedColorType::L8).ok()?;
|
||||
} else {
|
||||
let rgb = img.to_rgb8();
|
||||
image::ImageEncoder::write_image(enc, rgb.as_raw(), rgb.width(), rgb.height(), image::ExtendedColorType::Rgb8).ok()?;
|
||||
}
|
||||
jpeg(buf.into(), || embedded_icc(data).filter(|p| if grey { is_grey_icc(p) } else { is_rgb_icc(p) }), mode)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -340,6 +340,26 @@ fn cmyk_tiff_keeps_its_inks() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A placed greyscale TIFF went into PDFs as DeviceRGB, unlike a greyscale PNG or JPEG (#1426):
|
||||
/// a page holding only a black-and-white scan declared colour.
|
||||
#[test]
|
||||
fn grey_tiff_exports_as_device_gray() {
|
||||
let px: Vec<u8> = vec![0, 64, 128, 255, 255, 128, 64, 0];
|
||||
let mut tif = Vec::new();
|
||||
tiff::encoder::TiffEncoder::new(std::io::Cursor::new(&mut tif)).unwrap().write_image::<tiff::encoder::colortype::Gray8>(4, 2, &px).unwrap();
|
||||
let mut d = doc_with_text("x");
|
||||
place(&mut d, tif, (4, 2));
|
||||
for standard in [Standard::None, Standard::PdfX4] {
|
||||
let r = export_pdf_with_report(&d, &Cache::new(), &PdfOptions { standard, ..Default::default() }).unwrap();
|
||||
assert!(r.warnings.is_empty(), "{standard:?}: {:?}", r.warnings);
|
||||
assert_eq!(image_xobjects(&r.bytes), vec![("DeviceGray".to_string(), px.clone())], "{standard:?}");
|
||||
}
|
||||
// Recompressed, it stays a grey JPEG.
|
||||
let r = export_pdf_with_report(&d, &Cache::new(), &PdfOptions { compress_images: true, compress: false, ..Default::default() }).unwrap();
|
||||
let s = String::from_utf8_lossy(&r.bytes);
|
||||
assert!(s.contains("/DCTDecode") && s.contains("/DeviceGray") && !s.contains("/DeviceRGB"), "a grey JPEG");
|
||||
}
|
||||
|
||||
/// An 8×8 raster encoded as `fmt`; `alpha` gives it a transparent corner.
|
||||
fn raster(fmt: image::ImageFormat, alpha: bool) -> Vec<u8> {
|
||||
let img =
|
||||
|
||||
Reference in New Issue
Block a user