use crate::error::Result;
use crate::object::Object;
use super::context::ResolutionContext;
use super::intent::{DeviceColor, LogicalColor};
use super::resolved::ResolvedColor;
pub(crate) struct ColorResolver;
impl ColorResolver {
pub(crate) const fn new() -> Self {
Self
}
pub(crate) fn resolve(
&self,
color: &LogicalColor,
ctx: &ResolutionContext,
alpha: f32,
) -> Result<ResolvedColor> {
match color {
LogicalColor::Device(dev) => {
if let Some(resolved) = self.resolve_device_default_override(*dev, ctx, alpha)? {
return Ok(resolved);
}
Ok(device_to_rgba(*dev, alpha))
},
LogicalColor::Spaced { space, components } => {
self.resolve_spaced(space, components, ctx, alpha)
},
}
}
fn resolve_device_default_override(
&self,
dev: DeviceColor,
ctx: &ResolutionContext,
alpha: f32,
) -> Result<Option<ResolvedColor>> {
let (override_obj, components): (Option<&Object>, smallvec::SmallVec<[f32; 4]>) = match dev
{
DeviceColor::Gray(g) => (ctx.default_gray, smallvec::smallvec![g]),
DeviceColor::Rgb(r, g, b) => (ctx.default_rgb, smallvec::smallvec![r, g, b]),
DeviceColor::Cmyk(c, m, y, k) => (ctx.default_cmyk, smallvec::smallvec![c, m, y, k]),
};
let Some(space) = override_obj else {
return Ok(None);
};
if space.as_name().is_none() && space.as_array().is_none() {
return Ok(None);
}
Ok(Some(self.resolve_spaced(space, &components, ctx, alpha)?))
}
fn resolve_spaced(
&self,
space: &Object,
components: &[f32],
ctx: &ResolutionContext,
alpha: f32,
) -> Result<ResolvedColor> {
if let Some(name) = space.as_name() {
return Ok(resolve_device_alias(name, components, alpha));
}
let Some(arr) = space.as_array() else {
return Ok(first_as_gray(components, alpha));
};
let Some(type_name) = arr.first().and_then(|o| o.as_name()) else {
return Ok(first_as_gray(components, alpha));
};
match type_name {
"DeviceGray" | "G" | "CalGray" => Ok(first_as_gray(components, alpha)),
"DeviceRGB" | "RGB" | "CalRGB" => Ok(three_as_rgb(components, alpha)),
"DeviceCMYK" | "CMYK" => Ok(four_as_cmyk_native(components, alpha)),
"ICCBased" => self.resolve_iccbased(arr, components, ctx, alpha),
"Separation" | "DeviceN" => {
self.resolve_separation_or_devicen(arr, components, ctx, alpha)
},
"Indexed" => self.resolve_indexed(arr, components, ctx, alpha),
_ => Ok(first_as_gray(components, alpha)),
}
}
fn resolve_iccbased(
&self,
arr: &[Object],
components: &[f32],
ctx: &ResolutionContext,
alpha: f32,
) -> Result<ResolvedColor> {
let Some(stream_obj) = arr.get(1) else {
return Ok(first_as_gray(components, alpha));
};
let resolved_stream = match ctx.doc.resolve_object(stream_obj) {
Ok(o) => o,
Err(_) => return Ok(first_as_gray(components, alpha)),
};
let Some(dict) = resolved_stream.as_dict() else {
return Ok(first_as_gray(components, alpha));
};
let n = dict.get("N").and_then(|o| o.as_integer()).unwrap_or(3);
#[cfg(any(feature = "icc-qcms", feature = "icc-lcms2"))]
if n == 4 && components.len() >= 4 {
if let Ok(bytes) = resolved_stream.decode_stream_data() {
if let Some(profile) = crate::color::IccProfile::parse(bytes, 4) {
let profile = std::sync::Arc::new(profile);
let transform: std::sync::Arc<crate::color::Transform> =
if let Some(cache) = ctx.icc_transform_cache {
cache.get_or_build(&profile, ctx.rendering_intent)
} else {
std::sync::Arc::new(crate::color::Transform::new_srgb_target(
std::sync::Arc::clone(&profile),
ctx.rendering_intent,
))
};
if transform.has_cmm() {
let c = components[0].clamp(0.0, 1.0);
let m = components[1].clamp(0.0, 1.0);
let y = components[2].clamp(0.0, 1.0);
let k = components[3].clamp(0.0, 1.0);
let c_u8 = (c * 255.0).round() as u8;
let m_u8 = (m * 255.0).round() as u8;
let y_u8 = (y * 255.0).round() as u8;
let k_u8 = (k * 255.0).round() as u8;
let rgb = transform.convert_cmyk_pixel(c_u8, m_u8, y_u8, k_u8);
return Ok(ResolvedColor::IccCmyk {
r: rgb[0] as f32 / 255.0,
g: rgb[1] as f32 / 255.0,
b: rgb[2] as f32 / 255.0,
c,
m,
y,
k,
a: alpha,
});
}
}
}
}
#[cfg(any(feature = "icc-qcms", feature = "icc-lcms2"))]
if n == 3 && components.len() >= 3 {
if let Ok(bytes) = resolved_stream.decode_stream_data() {
if let Some(profile) = crate::color::IccProfile::parse(bytes, 3) {
let profile = std::sync::Arc::new(profile);
let transform: std::sync::Arc<crate::color::Transform> =
if let Some(cache) = ctx.icc_transform_cache {
cache.get_or_build(&profile, ctx.rendering_intent)
} else {
std::sync::Arc::new(crate::color::Transform::new_srgb_target(
std::sync::Arc::clone(&profile),
ctx.rendering_intent,
))
};
if transform.has_cmm() {
let r = components[0].clamp(0.0, 1.0);
let g = components[1].clamp(0.0, 1.0);
let b = components[2].clamp(0.0, 1.0);
let r_u8 = (r * 255.0).round() as u8;
let g_u8 = (g * 255.0).round() as u8;
let b_u8 = (b * 255.0).round() as u8;
let rgb = transform.convert_rgb_buffer(&[r_u8, g_u8, b_u8]);
if rgb.len() >= 3 {
return Ok(ResolvedColor::Rgba {
r: rgb[0] as f32 / 255.0,
g: rgb[1] as f32 / 255.0,
b: rgb[2] as f32 / 255.0,
a: alpha,
});
}
}
}
}
}
#[cfg(any(feature = "icc-qcms", feature = "icc-lcms2"))]
if n == 1 && !components.is_empty() {
if let Ok(bytes) = resolved_stream.decode_stream_data() {
if let Some(profile) = crate::color::IccProfile::parse(bytes, 1) {
let profile = std::sync::Arc::new(profile);
let transform: std::sync::Arc<crate::color::Transform> =
if let Some(cache) = ctx.icc_transform_cache {
cache.get_or_build(&profile, ctx.rendering_intent)
} else {
std::sync::Arc::new(crate::color::Transform::new_srgb_target(
std::sync::Arc::clone(&profile),
ctx.rendering_intent,
))
};
if transform.has_cmm() {
let g = components[0].clamp(0.0, 1.0);
let g_u8 = (g * 255.0).round() as u8;
let rgb = transform.convert_gray_buffer(&[g_u8]);
if rgb.len() >= 3 {
return Ok(ResolvedColor::Rgba {
r: rgb[0] as f32 / 255.0,
g: rgb[1] as f32 / 255.0,
b: rgb[2] as f32 / 255.0,
a: alpha,
});
}
}
}
}
}
match n {
1 if !components.is_empty() => Ok(first_as_gray(components, alpha)),
3 if components.len() >= 3 => Ok(three_as_rgb(components, alpha)),
4 if components.len() >= 4 => Ok(four_as_cmyk_native(components, alpha)),
_ => Ok(first_as_gray(components, alpha)),
}
}
fn resolve_separation_or_devicen(
&self,
arr: &[Object],
components: &[f32],
ctx: &ResolutionContext,
alpha: f32,
) -> Result<ResolvedColor> {
if components.is_empty() {
return Ok(ResolvedColor::Rgba {
r: 0.0,
g: 0.0,
b: 0.0,
a: alpha,
});
}
let type_name = arr.first().and_then(|o| o.as_name());
if matches!(type_name, Some("Separation"))
&& arr.get(1).and_then(|o| o.as_name()) == Some("None")
{
return Ok(ResolvedColor::Rgba {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
});
}
let invert_tint_fallback = |components: &[f32], alpha: f32| -> ResolvedColor {
let t = components.first().copied().unwrap_or(0.0);
let g = (1.0 - t).clamp(0.0, 1.0);
ResolvedColor::Rgba {
r: g,
g,
b: g,
a: alpha,
}
};
let alt_cs_obj = match arr.get(2) {
Some(o) => o,
None => return Ok(invert_tint_fallback(components, alpha)),
};
let func_obj = match arr.get(3) {
Some(o) => o,
None => return Ok(invert_tint_fallback(components, alpha)),
};
let func_resolved = match ctx.doc.resolve_object(func_obj) {
Ok(o) => o,
Err(_) => return Ok(invert_tint_fallback(components, alpha)),
};
let Some(func_dict) = func_resolved.as_dict() else {
return Ok(invert_tint_fallback(components, alpha));
};
let func_type = func_dict
.get("FunctionType")
.and_then(|o| o.as_integer())
.unwrap_or(-1);
let alt_cs_name = alt_cs_obj.as_name();
let altspace_values: Vec<f32> = match func_type {
2 => evaluate_type2(func_dict, components[0]),
4 => evaluate_type4(&func_resolved, components)?,
_ => return Ok(invert_tint_fallback(components, alpha)),
};
match alt_cs_name {
Some("DeviceCMYK") | Some("CMYK") if altspace_values.len() >= 4 => {
Ok(four_as_cmyk(&altspace_values, alpha, ctx))
},
Some("DeviceRGB") | Some("RGB") if altspace_values.len() >= 3 => {
Ok(three_as_rgb(&altspace_values, alpha))
},
Some("DeviceGray") | Some("G") if !altspace_values.is_empty() => {
Ok(first_as_gray(&altspace_values, alpha))
},
_ => {
if let Object::Array(_) = alt_cs_obj {
self.resolve_spaced(alt_cs_obj, &altspace_values, ctx, alpha)
} else {
Ok(first_as_gray(&altspace_values, alpha))
}
},
}
}
fn resolve_indexed(
&self,
arr: &[Object],
components: &[f32],
_ctx: &ResolutionContext,
alpha: f32,
) -> Result<ResolvedColor> {
let _ = arr;
if components.is_empty() {
return Ok(ResolvedColor::Rgba {
r: 0.0,
g: 0.0,
b: 0.0,
a: alpha,
});
}
let g = (components[0] / 255.0).clamp(0.0, 1.0);
Ok(ResolvedColor::Rgba {
r: g,
g,
b: g,
a: alpha,
})
}
}
fn device_to_rgba(dev: DeviceColor, alpha: f32) -> ResolvedColor {
match dev {
DeviceColor::Gray(g) => ResolvedColor::Rgba {
r: g,
g,
b: g,
a: alpha,
},
DeviceColor::Rgb(r, g, b) => ResolvedColor::Rgba { r, g, b, a: alpha },
DeviceColor::Cmyk(c, m, y, k) => ResolvedColor::Cmyk {
c: c.clamp(0.0, 1.0),
m: m.clamp(0.0, 1.0),
y: y.clamp(0.0, 1.0),
k: k.clamp(0.0, 1.0),
a: alpha,
},
}
}
fn resolve_device_alias(name: &str, components: &[f32], alpha: f32) -> ResolvedColor {
match name {
"DeviceGray" | "G" | "CalGray" if !components.is_empty() => {
first_as_gray(components, alpha)
},
"DeviceRGB" | "RGB" | "CalRGB" if components.len() >= 3 => three_as_rgb(components, alpha),
"DeviceCMYK" | "CMYK" if components.len() >= 4 => four_as_cmyk_native(components, alpha),
_ => first_as_gray(components, alpha),
}
}
fn first_as_gray(components: &[f32], alpha: f32) -> ResolvedColor {
let g = components.first().copied().unwrap_or(0.0).clamp(0.0, 1.0);
ResolvedColor::Rgba {
r: g,
g,
b: g,
a: alpha,
}
}
fn three_as_rgb(components: &[f32], alpha: f32) -> ResolvedColor {
ResolvedColor::Rgba {
r: components[0].clamp(0.0, 1.0),
g: components[1].clamp(0.0, 1.0),
b: components[2].clamp(0.0, 1.0),
a: alpha,
}
}
fn four_as_cmyk(components: &[f32], alpha: f32, ctx: &ResolutionContext) -> ResolvedColor {
let (r, g, b) =
cmyk_to_rgb_via_intent(components[0], components[1], components[2], components[3], ctx);
ResolvedColor::Rgba { r, g, b, a: alpha }
}
fn four_as_cmyk_native(components: &[f32], alpha: f32) -> ResolvedColor {
ResolvedColor::Cmyk {
c: components[0].clamp(0.0, 1.0),
m: components[1].clamp(0.0, 1.0),
y: components[2].clamp(0.0, 1.0),
k: components[3].clamp(0.0, 1.0),
a: alpha,
}
}
fn cmyk_to_rgb(c: f32, m: f32, y: f32, k: f32) -> (f32, f32, f32) {
let r = 1.0 - (c + k).min(1.0);
let g = 1.0 - (m + k).min(1.0);
let b = 1.0 - (y + k).min(1.0);
(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
}
pub(crate) fn cmyk_to_rgb_via_intent(
c: f32,
m: f32,
y: f32,
k: f32,
ctx: &ResolutionContext<'_>,
) -> (f32, f32, f32) {
#[cfg(any(feature = "icc-qcms", feature = "icc-lcms2"))]
if let Some(profile) = ctx.output_intent_cmyk {
let c_u8 = (c.clamp(0.0, 1.0) * 255.0).round() as u8;
let m_u8 = (m.clamp(0.0, 1.0) * 255.0).round() as u8;
let y_u8 = (y.clamp(0.0, 1.0) * 255.0).round() as u8;
let k_u8 = (k.clamp(0.0, 1.0) * 255.0).round() as u8;
let rgb = if let Some(cache) = ctx.icc_transform_cache {
let transform = cache.get_or_build(profile, ctx.rendering_intent);
transform.convert_cmyk_pixel(c_u8, m_u8, y_u8, k_u8)
} else {
let transform = crate::color::Transform::new_srgb_target(
std::sync::Arc::clone(profile),
ctx.rendering_intent,
);
transform.convert_cmyk_pixel(c_u8, m_u8, y_u8, k_u8)
};
return (rgb[0] as f32 / 255.0, rgb[1] as f32 / 255.0, rgb[2] as f32 / 255.0);
}
let _ = ctx;
cmyk_to_rgb(c, m, y, k)
}
fn evaluate_type2(dict: &std::collections::HashMap<String, Object>, x: f32) -> Vec<f32> {
let n = dict
.get("N")
.and_then(|o| o.as_real().or_else(|| o.as_integer().map(|i| i as f64)))
.unwrap_or(1.0) as f32;
let c0 = dict.get("C0").and_then(|o| o.as_array());
let c1 = dict.get("C1").and_then(|o| o.as_array());
let len = c0.map(|a| a.len()).max(c1.map(|a| a.len())).unwrap_or(1);
let mut out = Vec::with_capacity(len);
let x_pow = if n == 1.0 { x } else { x.powf(n) };
for j in 0..len {
let c0j = c0.and_then(|a| a.get(j)).map(object_to_f32).unwrap_or(0.0);
let c1j = c1.and_then(|a| a.get(j)).map(object_to_f32).unwrap_or(1.0);
out.push(c0j + x_pow * (c1j - c0j));
}
out
}
fn evaluate_type4(func_obj: &Object, components: &[f32]) -> Result<Vec<f32>> {
let Object::Stream { dict, .. } = func_obj else {
return Ok(components.to_vec());
};
let bytes = func_obj.decode_stream_data()?;
let domain = dict
.get("Domain")
.and_then(|o| o.as_array())
.map(|a| array_to_pairs(a))
.unwrap_or_default();
let range = dict
.get("Range")
.and_then(|o| o.as_array())
.map(|a| array_to_pairs(a))
.unwrap_or_default();
let inputs: Vec<f64> = components.iter().map(|&v| v as f64).collect();
let out = crate::functions::evaluate_type4_clamped(&bytes, &inputs, &domain, &range)?;
Ok(out.into_iter().map(|v| v as f32).collect())
}
fn array_to_pairs(arr: &[Object]) -> Vec<[f64; 2]> {
arr.chunks_exact(2)
.map(|c| [object_to_f64(&c[0]), object_to_f64(&c[1])])
.collect()
}
fn object_to_f32(o: &Object) -> f32 {
object_to_f64(o) as f32
}
fn object_to_f64(o: &Object) -> f64 {
o.as_real()
.or_else(|| o.as_integer().map(|i| i as f64))
.unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rendering::resolution::test_support::fixture_doc;
use std::collections::HashMap;
fn ctx<'a>(
doc: &'a crate::document::PdfDocument,
spaces: &'a HashMap<String, Object>,
) -> ResolutionContext<'a> {
ResolutionContext::new(doc, spaces)
}
fn assert_rgba(c: ResolvedColor, r: f32, g: f32, b: f32, a: f32) {
let (rr, gg, bb, aa) = match c {
ResolvedColor::Rgba { r, g, b, a } => (r, g, b, a),
ResolvedColor::Cmyk { c, m, y, k, a } => {
let rr = (1.0 - (c + k).min(1.0)).clamp(0.0, 1.0);
let gg = (1.0 - (m + k).min(1.0)).clamp(0.0, 1.0);
let bb = (1.0 - (y + k).min(1.0)).clamp(0.0, 1.0);
(rr, gg, bb, a)
},
other => panic!("expected Rgba or Cmyk; got {other:?}"),
};
assert!((rr - r).abs() < 1e-3, "r: got {rr}, want {r}");
assert!((gg - g).abs() < 1e-3, "g: got {gg}, want {g}");
assert!((bb - b).abs() < 1e-3, "b: got {bb}, want {b}");
assert!((aa - a).abs() < 1e-3, "a: got {aa}, want {a}");
}
#[test]
fn resolves_device_gray_logical_color() {
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let lc = LogicalColor::Device(DeviceColor::Gray(0.42));
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 0.9).unwrap();
assert_rgba(c, 0.42, 0.42, 0.42, 0.9);
}
#[test]
fn resolves_device_rgb_logical_color() {
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let lc = LogicalColor::Device(DeviceColor::Rgb(1.0, 0.5, 0.25));
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 1.0).unwrap();
assert_rgba(c, 1.0, 0.5, 0.25, 1.0);
}
#[test]
fn resolves_device_cmyk_via_additive_clamp() {
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let lc = LogicalColor::Device(DeviceColor::Cmyk(1.0, 0.0, 0.0, 0.0));
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 1.0).unwrap();
assert_rgba(c, 0.0, 1.0, 1.0, 1.0);
}
#[test]
fn resolves_spaced_device_alias_as_rgb() {
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let space = Object::Name("DeviceRGB".to_string());
let lc = LogicalColor::Spaced {
space: &space,
components: smallvec::smallvec![0.2, 0.4, 0.6],
};
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 1.0).unwrap();
assert_rgba(c, 0.2, 0.4, 0.6, 1.0);
}
#[test]
fn separation_with_type2_cmyk_alternate_uses_function() {
let mut func_dict: HashMap<String, Object> = HashMap::new();
func_dict.insert("FunctionType".into(), Object::Integer(2));
func_dict.insert("N".into(), Object::Integer(1));
func_dict.insert(
"C0".into(),
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(0.0),
]),
);
func_dict.insert(
"C1".into(),
Object::Array(vec![
Object::Real(0.0),
Object::Real(1.0),
Object::Real(0.0),
Object::Real(0.0),
]),
);
let func_obj = Object::Dictionary(func_dict);
let arr = vec![
Object::Name("Separation".into()),
Object::Name("SpotInk".into()),
Object::Name("DeviceCMYK".into()),
func_obj,
];
let space = Object::Array(arr);
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let lc = LogicalColor::Spaced {
space: &space,
components: smallvec::smallvec![1.0],
};
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 1.0).unwrap();
assert_rgba(c, 1.0, 0.0, 1.0, 1.0);
}
#[test]
fn separation_with_type4_calculator_evaluates_program() {
let program = b"{ 0.0 exch 0.0 0.0 }";
let mut func_dict: HashMap<String, Object> = HashMap::new();
func_dict.insert("FunctionType".into(), Object::Integer(4));
func_dict
.insert("Domain".into(), Object::Array(vec![Object::Integer(0), Object::Integer(1)]));
func_dict.insert(
"Range".into(),
Object::Array(vec![
Object::Integer(0),
Object::Integer(1),
Object::Integer(0),
Object::Integer(1),
Object::Integer(0),
Object::Integer(1),
Object::Integer(0),
Object::Integer(1),
]),
);
let func_obj = Object::Stream {
dict: func_dict,
data: program.to_vec().into(),
};
let arr = vec![
Object::Name("Separation".into()),
Object::Name("MagentaSpot".into()),
Object::Name("DeviceCMYK".into()),
func_obj,
];
let space = Object::Array(arr);
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let lc = LogicalColor::Spaced {
space: &space,
components: smallvec::smallvec![1.0],
};
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 1.0).unwrap();
assert_rgba(c, 1.0, 0.0, 1.0, 1.0);
}
#[test]
fn separation_full_tint_with_type4_no_longer_renders_solid_black() {
let program = b"{ 0.0 exch 0.0 0.0 }";
let mut func_dict: HashMap<String, Object> = HashMap::new();
func_dict.insert("FunctionType".into(), Object::Integer(4));
let func_obj = Object::Stream {
dict: func_dict,
data: program.to_vec().into(),
};
let arr = vec![
Object::Name("Separation".into()),
Object::Name("MagentaSpot".into()),
Object::Name("DeviceCMYK".into()),
func_obj,
];
let space = Object::Array(arr);
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let lc = LogicalColor::Spaced {
space: &space,
components: smallvec::smallvec![1.0],
};
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 1.0).unwrap();
let (r, g, b) = match c {
ResolvedColor::Rgba { r, g, b, .. } => (r, g, b),
ResolvedColor::Cmyk { c, m, y, k, .. } => {
let rr = (1.0 - (c + k).min(1.0)).clamp(0.0, 1.0);
let gg = (1.0 - (m + k).min(1.0)).clamp(0.0, 1.0);
let bb = (1.0 - (y + k).min(1.0)).clamp(0.0, 1.0);
(rr, gg, bb)
},
other => panic!("expected Rgba or Cmyk; got {other:?}"),
};
assert!(
!(r < 0.01 && g < 0.01 && b < 0.01),
"full-tint Type-4 spot must not render solid black; got ({r}, {g}, {b})"
);
}
#[test]
fn separation_none_resolves_to_fully_transparent_for_composite() {
let arr = vec![
Object::Name("Separation".into()),
Object::Name("None".into()),
Object::Name("DeviceGray".into()),
Object::Dictionary({
let mut d = HashMap::new();
d.insert("FunctionType".into(), Object::Integer(2));
d
}),
];
let space = Object::Array(arr);
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let lc = LogicalColor::Spaced {
space: &space,
components: smallvec::smallvec![0.5],
};
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 0.9).unwrap();
match c {
ResolvedColor::Rgba { a, .. } => {
assert!((a - 0.0).abs() < 1e-6, "/None composite alpha must be 0");
},
other => panic!("expected Rgba; got {other:?}"),
}
}
#[test]
fn separation_with_unknown_function_type_falls_back_to_gray() {
let mut func_dict: HashMap<String, Object> = HashMap::new();
func_dict.insert("FunctionType".into(), Object::Integer(99));
let func_obj = Object::Dictionary(func_dict);
let arr = vec![
Object::Name("Separation".into()),
Object::Name("Whatever".into()),
Object::Name("DeviceCMYK".into()),
func_obj,
];
let space = Object::Array(arr);
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let lc = LogicalColor::Spaced {
space: &space,
components: smallvec::smallvec![0.5],
};
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 1.0).unwrap();
assert_rgba(c, 0.5, 0.5, 0.5, 1.0);
}
#[test]
fn iccbased_with_n4_routes_through_cmyk_fallback() {
let mut stream_dict: HashMap<String, Object> = HashMap::new();
stream_dict.insert("N".into(), Object::Integer(4));
let icc_stream = Object::Stream {
dict: stream_dict,
data: Vec::new().into(),
};
let arr = vec![Object::Name("ICCBased".into()), icc_stream];
let space = Object::Array(arr);
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let lc = LogicalColor::Spaced {
space: &space,
components: smallvec::smallvec![1.0, 0.0, 0.0, 0.0],
};
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 1.0).unwrap();
assert_rgba(c, 0.0, 1.0, 1.0, 1.0);
}
#[test]
fn alpha_passthrough_into_rgba() {
let doc = fixture_doc();
let spaces = HashMap::new();
let resolver = ColorResolver::new();
let lc = LogicalColor::Device(DeviceColor::Gray(0.5));
let c = resolver.resolve(&lc, &ctx(&doc, &spaces), 0.3).unwrap();
match c {
ResolvedColor::Rgba { a, .. } => assert!((a - 0.3).abs() < 1e-6),
_ => panic!("expected Rgba"),
}
}
#[test]
fn cmyk_to_rgb_via_intent_with_no_output_intent_matches_additive_clamp() {
let doc = fixture_doc();
let spaces = HashMap::new();
let ctx = ResolutionContext::new(&doc, &spaces);
let (r, g, b) = super::cmyk_to_rgb_via_intent(0.25, 0.0, 0.0, 0.0, &ctx);
assert!((r - 0.75).abs() < 1e-6);
assert!((g - 1.0).abs() < 1e-6);
assert!((b - 1.0).abs() < 1e-6);
}
#[cfg(any(feature = "icc-qcms", feature = "icc-lcms2"))]
#[test]
fn cmyk_to_rgb_via_intent_falls_back_when_profile_has_no_cmm() {
let doc = fixture_doc();
let spaces = HashMap::new();
let mut header_only = vec![0u8; 128];
header_only[8..12].copy_from_slice(&0x04000000u32.to_be_bytes());
header_only[12..16].copy_from_slice(b"prtr");
header_only[16..20].copy_from_slice(b"CMYK");
header_only[20..24].copy_from_slice(b"Lab ");
header_only[36..40].copy_from_slice(b"acsp");
let profile = std::sync::Arc::new(
crate::color::IccProfile::parse(header_only, 4).expect("stub parses"),
);
let ctx = ResolutionContext::new(&doc, &spaces).with_output_intent(Some(&profile));
let (r, g, b) = super::cmyk_to_rgb_via_intent(0.25, 0.0, 0.0, 0.0, &ctx);
assert!((r - 0.75).abs() < 0.01, "got r={r}");
assert!((g - 1.0).abs() < 0.01, "got g={g}");
assert!((b - 1.0).abs() < 0.01, "got b={b}");
}
}