Skip to content
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
14 changes: 14 additions & 0 deletions crates/pdf-color-space/src/color_space.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ impl ColorSpace {
matches!(self, Self::DeviceGray | Self::DeviceRGB | Self::DeviceCMYK)
}

/// Returns the device space that draws this space's samples unchanged, if there is one.
///
/// A device space is its own equivalent. An ICCBased space draws through its
/// alternate until ICC colour management exists, so it has one when that alternate
/// does.
#[must_use]
pub fn device_equivalent(&self) -> Option<Self> {
match self {
Self::ICCBased(icc) => icc.device_equivalent(),
space if space.is_device_space() => Some(space.clone()),
_ => None,
}
}

pub fn apply(&self, components: &[f32]) -> Result<Color, ColorSpaceError> {
match self {
ColorSpace::Indexed(indexed) => indexed.apply(components),
Expand Down
17 changes: 17 additions & 0 deletions crates/pdf-color-space/src/icc_based_color_space.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ impl FromPdfObject for ICCBasedColorSpace {
}

impl ICCBasedColorSpace {
/// Returns the device space this space draws as: its alternate's device equivalent,
/// or the device space with its number of components when it names no alternate.
///
/// An alternate with a different number of components has none.
pub(crate) fn device_equivalent(&self) -> Option<ColorSpace> {
let space = match self.alternate_space.as_deref() {
Some(alternate) => alternate.device_equivalent()?,
None => match self.num_components {
1 => ColorSpace::DeviceGray,
3 => ColorSpace::DeviceRGB,
4 => ColorSpace::DeviceCMYK,
_ => return None,
},
};
(space.num_color_components() == self.num_components).then_some(space)
}

pub(crate) fn apply(&self, components: &[f32]) -> Result<Color, ColorSpaceError> {
if components.len() != self.num_components {
return Err(ColorSpaceError::InsufficientComponents(
Expand Down
22 changes: 11 additions & 11 deletions crates/pdf-decode/src/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,16 @@ impl DecodeMap {
out
}

/// Applies the decode ranges while retaining their floating-point component domains.
#[must_use]
pub fn apply_to_f32(&self, samples: &[u8], sample_max: u8) -> Vec<f32> {
if self.ranges.is_empty() {
return Vec::new();
}

samples
/// Applies the decode ranges to `samples`, writing their floating-point component values
/// into `out`. Writing stops at the shorter of the two slices.
pub fn apply_to_f32(&self, samples: &[u8], sample_max: u8, out: &mut [f32]) {
for ((sample, range), value) in samples
.iter()
.zip(self.ranges.iter().cycle())
.map(|(sample, range)| range.map_f32(*sample, sample_max))
.collect()
.zip(out.iter_mut())
{
*value = range.map_f32(*sample, sample_max);
}
}
}

Expand Down Expand Up @@ -98,6 +96,8 @@ mod tests {
ranges: vec![DecodeRange::new(-100.0, 100.0).unwrap()],
};

assert_eq!(map.apply_to_f32(&[0, 255], 255), vec![-100.0, 100.0]);
let mut values = [0.0; 2];
map.apply_to_f32(&[0, 255], 255, &mut values);
assert_eq!(values, [-100.0, 100.0]);
}
}
194 changes: 109 additions & 85 deletions crates/pdf-image/src/decoded_samples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use pdf_decode::{
};
use pdf_filter::image_payload::ImagePayload;

use crate::error::PdfImageError;
use crate::error::{ImageRasterError, PdfImageError};
use crate::image_metadata::ImageMetadata;
use crate::jpx_image::{JpxDisplay, JpxImage};

Expand Down Expand Up @@ -43,25 +43,22 @@ impl DecodedSamples {
}

match &metadata.color_space {
Some(ColorSpace::Indexed(indexed)) => {
if indexed.base.is_device_space() {
Self::decode_indexed(
raw_data,
metadata,
indexed.base.num_color_components(),
indexed.hival,
&indexed.lookup,
)
} else {
Self::decode_indexed_rgba(raw_data, metadata, indexed)
Some(ColorSpace::Indexed(indexed)) => match indexed.base.device_equivalent() {
Some(base) => Self::decode_indexed(
raw_data,
metadata,
base.num_color_components(),
indexed.hival,
&indexed.lookup,
),
None => Self::decode_indexed_rgba(raw_data, metadata, indexed),
},
Some(color_space) => match color_space.device_equivalent() {
Some(device) => {
Self::decode_direct(raw_data, metadata, device.num_color_components())
}
}
Some(color_space) if !color_space.is_device_space() => {
Self::decode_direct_rgba(raw_data, metadata, color_space)
}
Some(color_space) => {
Self::decode_direct(raw_data, metadata, color_space.num_color_components())
}
None => Self::decode_direct_rgba(raw_data, metadata, color_space),
},
None => Self::decode_direct(raw_data, metadata, 1),
}
}
Expand All @@ -85,36 +82,21 @@ impl DecodedSamples {
metadata.size.height(),
)?;

if alpha.is_none() && color_space.is_device_space() {
if alpha.is_none() && color_space.device_equivalent().is_some() {
return Ok(Self {
num_color_components: components,
image_data: color.into(),
is_rgba: false,
});
}

// An Indexed space reads its component as a palette index rather than a
// fraction of full scale, so the sample passes through unnormalized.
let decoded = match color_space {
ColorSpace::Indexed(_) => color.iter().map(|sample| f32::from(*sample)).collect(),
ref color_space => Self::default_color_components(&color, u8::MAX, color_space),
};
let mut image_data = Vec::new();
let pixels = decoded.len().checked_div(components.max(1)).unwrap_or(0);
image_data
.try_reserve_exact(pixels.saturating_mul(4))
.map_err(|_| PdfImageError::JpxImageTooLarge {
pixels,
channels: 4,
})?;
for (index, pixel) in decoded.chunks_exact(components.max(1)).enumerate() {
let [red, green, blue, _] = color_space.apply(pixel)?.to_rgba8();
let opacity = alpha
.as_ref()
.and_then(|plane| plane.get(index).copied())
.unwrap_or(u8::MAX);
image_data.extend_from_slice(&[red, green, blue, opacity]);
}
// PDF ignores `/Decode` for a JPX image.
let image_data = ComponentDomain::new(&color_space, None).to_rgba(
&color,
u8::MAX,
&color_space,
alpha.as_deref(),
)?;

Ok(Self {
num_color_components: 4,
Expand Down Expand Up @@ -284,20 +266,12 @@ impl DecodedSamples {
let components = color_space.num_color_components();
let sample_codes = Self::decode_image_sample_codes(raw_data, components, metadata)?;
let sample_max = Self::sample_max(metadata.sample_bits())?;
let decoded = metadata.decode.as_ref().map_or_else(
|| Self::default_color_components(sample_codes.as_ref(), sample_max, color_space),
|decode| decode.apply_to_f32(sample_codes.as_ref(), sample_max),
);
let mut image_data = Vec::with_capacity(
decoded
.len()
.checked_div(components)
.unwrap_or(0)
.saturating_mul(4),
);
for pixel in decoded.chunks_exact(components) {
image_data.extend_from_slice(&color_space.apply(pixel)?.to_rgba8());
}
let image_data = ComponentDomain::new(color_space, metadata.decode.as_ref()).to_rgba(
&sample_codes,
sample_max,
color_space,
None,
)?;

Ok(Self {
num_color_components: 4,
Expand All @@ -306,35 +280,6 @@ impl DecodedSamples {
})
}

/// Applies the implicit decode domains for non-device color spaces.
fn default_color_components(
samples: &[u8],
sample_max: u8,
color_space: &ColorSpace,
) -> Vec<f32> {
let denominator = f32::from(sample_max.max(1));
match color_space {
ColorSpace::Lab(lab) => samples
.as_chunks::<3>()
.0
.iter()
.flat_map(|&[l, a, b]| {
let [amin, amax, bmin, bmax] = lab.range;
let normalized = |sample| f32::from(sample) / denominator;
[
normalized(l) * 100.0,
amin + normalized(a) * (amax - amin),
bmin + normalized(b) * (bmax - bmin),
]
})
.collect(),
_ => samples
.iter()
.map(|sample| f32::from(*sample) / denominator)
.collect(),
}
}

/// Applies an explicit decode map or the implicit PDF identity/inverted default.
fn apply_decode(
sample_codes: Bytes,
Expand Down Expand Up @@ -425,6 +370,85 @@ impl DecodedSamples {
}
}

/// How a non-device color space reads one pixel's samples as color components.
enum ComponentDomain<'a> {
/// An explicit `/Decode` array maps each sample into its component range.
Decode(&'a DecodeMap),
/// An Indexed space reads its component as a palette index rather than a
/// fraction of full scale, so the sample passes through unnormalized.
Index,
/// `L*a*b*` samples span L* 0 to 100 and the space's `[amin amax bmin bmax]` ranges.
Lab([f32; 4]),
/// Samples are fractions of full scale.
Fraction,
}

impl<'a> ComponentDomain<'a> {
/// Selects the domain of `color_space`, or of `decode` when the image gives one.
fn new(color_space: &ColorSpace, decode: Option<&'a DecodeMap>) -> Self {
match (decode, color_space) {
(Some(decode), _) => Self::Decode(decode),
(None, ColorSpace::Indexed(_)) => Self::Index,
(None, ColorSpace::Lab(lab)) => Self::Lab(lab.range),
(None, _) => Self::Fraction,
}
}

/// Writes the color components of one pixel's samples into `components`.
fn read(&self, pixel: &[u8], sample_max: u8, components: &mut [f32]) {
let denominator = match self {
Self::Index => 1.0,
_ => f32::from(sample_max.max(1)),
};
let normalized = |sample: u8| f32::from(sample) / denominator;
match self {
Self::Decode(decode) => decode.apply_to_f32(pixel, sample_max, components),
Self::Lab([amin, amax, bmin, bmax]) => {
if let ([l, a, b, ..], [cl, ca, cb, ..]) = (pixel, components) {
*cl = normalized(*l) * 100.0;
*ca = amin + normalized(*a) * (amax - amin);
*cb = bmin + normalized(*b) * (bmax - bmin);
}
}
Self::Index | Self::Fraction => {
for (sample, component) in pixel.iter().zip(components.iter_mut()) {
*component = normalized(*sample);
}
}
}
}

/// Converts interleaved samples through `color_space` into RGBA, one pixel at a time,
/// so no intermediate copy of the whole image is held in floating point.
///
/// Opacity comes from `alpha`, one value per pixel, when it is given, and otherwise
/// from the color the space produces.
fn to_rgba(
&self,
samples: &[u8],
sample_max: u8,
color_space: &ColorSpace,
alpha: Option<&[u8]>,
) -> Result<Vec<u8>, PdfImageError> {
let components = color_space.num_color_components().max(1);
let pixels = samples.len().checked_div(components).unwrap_or(0);
let mut image_data = Vec::new();
image_data
.try_reserve_exact(pixels.saturating_mul(4))
.map_err(|_| PdfImageError::Raster(ImageRasterError::ResourceLimit))?;
let mut pixel = vec![0.0; components];
for (index, samples) in samples.chunks_exact(components).enumerate() {
self.read(samples, sample_max, &mut pixel);
let [red, green, blue, color_alpha] = color_space.apply(&pixel)?.to_rgba8();
let opacity = alpha
.and_then(|plane| plane.get(index).copied())
.unwrap_or(color_alpha);
image_data.extend_from_slice(&[red, green, blue, opacity]);
}
Ok(image_data)
}
}

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
Expand Down
4 changes: 3 additions & 1 deletion crates/pdf-image/src/image_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ fn image_from_rgba(
};
}

let mut rgba = data.get(..byte_len).map_or_else(Vec::new, <[u8]>::to_vec);
// Freshly decoded samples are not shared, so this takes over their buffer without a copy.
let mut rgba = Vec::from(data);
rgba.truncate(byte_len);
if let Some(mask) = soft_mask {
for ([_, _, _, alpha], mask_alpha) in rgba
.as_chunks_mut::<4>()
Expand Down
Loading