use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use crate::color::{
CmykRetargetTransform, IccProfile, RenderingIntent, SrgbToCmykTransform, Transform,
};
use crate::document::PdfDocument;
use crate::object::Object;
pub(crate) struct IccTransformCache {
entries: RefCell<HashMap<(u64, RenderingIntent), Arc<Transform>>>,
srgb_to_cmyk_entries:
RefCell<HashMap<(u64, RenderingIntent), Option<Arc<SrgbToCmykTransform>>>>,
cmyk_retarget_entries: RefCell<
HashMap<
((u8, usize, u64), (u8, usize, u64), RenderingIntent),
Option<Arc<CmykRetargetTransform>>,
>,
>,
#[cfg(feature = "test-support")]
pub(crate) build_count: std::cell::Cell<usize>,
#[cfg(feature = "test-support")]
pub(crate) lookup_count: std::cell::Cell<usize>,
#[cfg(feature = "test-support")]
pub(crate) cmyk_retarget_build_count: std::cell::Cell<usize>,
}
impl IccTransformCache {
pub(crate) fn new() -> Self {
Self {
entries: RefCell::new(HashMap::new()),
srgb_to_cmyk_entries: RefCell::new(HashMap::new()),
cmyk_retarget_entries: RefCell::new(HashMap::new()),
#[cfg(feature = "test-support")]
build_count: std::cell::Cell::new(0),
#[cfg(feature = "test-support")]
lookup_count: std::cell::Cell::new(0),
#[cfg(feature = "test-support")]
cmyk_retarget_build_count: std::cell::Cell::new(0),
}
}
pub(crate) fn get_or_build(
&self,
profile: &Arc<IccProfile>,
intent: RenderingIntent,
) -> Arc<Transform> {
#[cfg(feature = "test-support")]
self.lookup_count.set(self.lookup_count.get() + 1);
let key = (profile.content_hash(), intent);
if let Some(t) = self.entries.borrow().get(&key).cloned() {
return t;
}
let t = Arc::new(Transform::new_srgb_target(Arc::clone(profile), intent));
self.entries.borrow_mut().insert(key, Arc::clone(&t));
#[cfg(feature = "test-support")]
self.build_count.set(self.build_count.get() + 1);
t
}
pub(crate) fn get_or_build_srgb_to_cmyk(
&self,
dst_profile: &Arc<IccProfile>,
intent: RenderingIntent,
) -> Option<Arc<SrgbToCmykTransform>> {
let key = (dst_profile.content_hash(), intent);
if let Some(slot) = self.srgb_to_cmyk_entries.borrow().get(&key) {
return slot.clone();
}
let built = SrgbToCmykTransform::new(Arc::clone(dst_profile), intent).map(Arc::new);
self.srgb_to_cmyk_entries
.borrow_mut()
.insert(key, built.clone());
built
}
pub(crate) fn get_or_build_cmyk_retarget(
&self,
src_profile: &Arc<IccProfile>,
dst_profile: &Arc<IccProfile>,
intent: RenderingIntent,
) -> Option<Arc<CmykRetargetTransform>> {
let src_key = (
src_profile.n_components(),
src_profile.bytes().len(),
src_profile.content_hash(),
);
let dst_key = (
dst_profile.n_components(),
dst_profile.bytes().len(),
dst_profile.content_hash(),
);
let key = (src_key, dst_key, intent);
if let Some(slot) = self.cmyk_retarget_entries.borrow().get(&key) {
return slot.clone();
}
let built =
CmykRetargetTransform::new(Arc::clone(src_profile), Arc::clone(dst_profile), intent)
.map(Arc::new);
self.cmyk_retarget_entries
.borrow_mut()
.insert(key, built.clone());
#[cfg(feature = "test-support")]
self.cmyk_retarget_build_count
.set(self.cmyk_retarget_build_count.get() + 1);
built
}
pub(crate) fn clear(&self) {
self.entries.borrow_mut().clear();
self.srgb_to_cmyk_entries.borrow_mut().clear();
self.cmyk_retarget_entries.borrow_mut().clear();
#[cfg(feature = "test-support")]
self.build_count.set(0);
#[cfg(feature = "test-support")]
self.lookup_count.set(0);
#[cfg(feature = "test-support")]
self.cmyk_retarget_build_count.set(0);
}
#[cfg(feature = "test-support")]
pub(crate) fn build_count(&self) -> usize {
self.build_count.get()
}
#[cfg(feature = "test-support")]
pub(crate) fn lookup_count(&self) -> usize {
self.lookup_count.get()
}
#[cfg(feature = "test-support")]
pub(crate) fn cmyk_retarget_build_count(&self) -> usize {
self.cmyk_retarget_build_count.get()
}
}
impl Default for IccTransformCache {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct ResolutionContext<'a> {
pub(crate) doc: &'a PdfDocument,
pub(crate) color_spaces: &'a HashMap<String, Object>,
pub(crate) output_intent_cmyk: Option<&'a Arc<IccProfile>>,
pub(crate) rendering_intent: RenderingIntent,
pub(crate) default_gray: Option<&'a Object>,
pub(crate) default_rgb: Option<&'a Object>,
pub(crate) default_cmyk: Option<&'a Object>,
pub(crate) icc_transform_cache: Option<&'a IccTransformCache>,
}
impl<'a> ResolutionContext<'a> {
pub(crate) fn new(doc: &'a PdfDocument, color_spaces: &'a HashMap<String, Object>) -> Self {
Self {
doc,
color_spaces,
output_intent_cmyk: None,
rendering_intent: RenderingIntent::default(),
default_gray: None,
default_rgb: None,
default_cmyk: None,
icc_transform_cache: None,
}
}
pub(crate) fn with_icc_transform_cache(mut self, cache: Option<&'a IccTransformCache>) -> Self {
self.icc_transform_cache = cache;
self
}
pub(crate) fn with_output_intent(mut self, profile: Option<&'a Arc<IccProfile>>) -> Self {
self.output_intent_cmyk = profile;
self
}
pub(crate) fn with_rendering_intent(mut self, intent: RenderingIntent) -> Self {
self.rendering_intent = intent;
self
}
pub(crate) fn with_defaults(
mut self,
gray: Option<&'a Object>,
rgb: Option<&'a Object>,
cmyk: Option<&'a Object>,
) -> Self {
self.default_gray = gray;
self.default_rgb = rgb;
self.default_cmyk = cmyk;
self
}
}
#[cfg(test)]
mod tests {
use super::super::test_support::fixture_doc;
use super::*;
#[test]
fn context_carries_empty_color_spaces() {
let doc = fixture_doc();
let color_spaces = HashMap::new();
let ctx = ResolutionContext::new(&doc, &color_spaces);
assert!(ctx.color_spaces.is_empty());
assert!(ctx.output_intent_cmyk.is_none());
assert_eq!(ctx.rendering_intent, RenderingIntent::RelativeColorimetric);
assert!(ctx.default_gray.is_none());
assert!(ctx.default_rgb.is_none());
assert!(ctx.default_cmyk.is_none());
}
#[test]
fn context_borrows_color_space_map() {
let doc = fixture_doc();
let mut color_spaces = HashMap::new();
color_spaces.insert("CS1".to_string(), Object::Name("DeviceCMYK".to_string()));
let ctx = ResolutionContext::new(&doc, &color_spaces);
assert!(ctx.color_spaces.contains_key("CS1"));
let ctx2 = ResolutionContext::new(&doc, &color_spaces);
assert_eq!(ctx2.color_spaces.len(), 1);
}
#[test]
fn context_carries_output_intent_when_set() {
let doc = fixture_doc();
let color_spaces = HashMap::new();
let profile = Arc::new(
IccProfile::parse(super::tests::header_only_cmyk_profile_bytes(), 4)
.expect("header-only stub profile parses"),
);
let ctx = ResolutionContext::new(&doc, &color_spaces).with_output_intent(Some(&profile));
assert!(ctx.output_intent_cmyk.is_some());
}
#[test]
fn with_rendering_intent_overrides_default() {
let doc = fixture_doc();
let color_spaces = HashMap::new();
let ctx = ResolutionContext::new(&doc, &color_spaces)
.with_rendering_intent(RenderingIntent::AbsoluteColorimetric);
assert_eq!(ctx.rendering_intent, RenderingIntent::AbsoluteColorimetric);
}
#[test]
fn with_defaults_attaches_each_override_independently() {
let doc = fixture_doc();
let color_spaces = HashMap::new();
let gray = Object::Name("DeviceGray".to_string());
let cmyk = Object::Name("DeviceCMYK".to_string());
let ctx = ResolutionContext::new(&doc, &color_spaces).with_defaults(
Some(&gray),
None,
Some(&cmyk),
);
assert!(ctx.default_gray.is_some());
assert!(ctx.default_rgb.is_none());
assert!(ctx.default_cmyk.is_some());
}
pub(crate) fn header_only_cmyk_profile_bytes() -> Vec<u8> {
let mut v = vec![0u8; 128];
v[8..12].copy_from_slice(&0x04000000u32.to_be_bytes());
v[12..16].copy_from_slice(b"prtr");
v[16..20].copy_from_slice(b"CMYK");
v[20..24].copy_from_slice(b"Lab ");
v[36..40].copy_from_slice(b"acsp");
v
}
}