mod tiling;
pub use tiling::{TileRange, TilingPattern};
use crate::color::ColorSpace;
use crate::function::FunctionCache;
use crate::names;
use crate::shading::{Shading, ShadingSource};
use kurbo::Affine;
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Dict, Object, Resolve};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Pattern {
Tiling(Box<TilingPattern>),
Shading(Box<ShadingPattern>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ShadingPattern {
pub shading: Arc<Shading>,
pub matrix: Affine,
pub ext_g_state: Option<Dict>,
}
impl Pattern {
#[must_use]
pub fn matrix(&self) -> Affine {
match self {
Self::Tiling(p) => p.matrix,
Self::Shading(p) => p.matrix,
}
}
#[must_use]
pub fn load<R: Resolve>(
obj: &Object,
parent_matrix: Affine,
resources: Option<&Dict>,
r: &R,
functions: &mut FunctionCache,
limits: &Limits,
diags: &mut Diagnostics,
) -> Option<Self> {
let resolved = obj.resolve(r).ok()?;
let dict = match &*resolved {
Object::Dict(d) => d.clone(),
Object::Stream(s) => s.dict.clone(),
_ => return None,
};
let own = dict.matrix(names::MATRIX, r);
let matrix = parent_matrix * own;
match dict.int(names::PATTERN_TYPE, r)? {
1 => {
let stream = resolved.as_stream()?;
Some(Self::Tiling(Box::new(TilingPattern::load(
stream, matrix, r, limits, diags,
))))
}
2 => {
let shading_obj = dict.raw(names::SHADING)?;
let shading = Shading::load(
shading_obj,
resources,
ShadingSource::Pattern,
r,
functions,
limits,
diags,
)?;
Some(Self::Shading(Box::new(ShadingPattern {
shading: Arc::new(shading),
matrix,
ext_g_state: dict.dict(names::EXT_G_STATE, r),
})))
}
_ => None,
}
}
}
#[must_use]
pub fn uncolored_pattern_rgb(
space: &ColorSpace,
components: &[f32],
colored_tiling: bool,
) -> crate::color::Rgb {
if let ColorSpace::Pattern(p) = space
&& let Some(rgb) = p.to_rgb(components)
{
return rgb;
}
if colored_tiling {
crate::color::Rgb {
r: 191.0 / 255.0,
g: 191.0 / 255.0,
b: 191.0 / 255.0,
}
} else {
crate::color::Rgb {
r: 1.0,
g: 1.0,
b: 1.0,
}
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{Pattern, uncolored_pattern_rgb};
use crate::color::{ColorSpace, PatternSpace};
use crate::function::FunctionCache;
use kurbo::Affine;
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Dict, Name, NoResolve, Object};
fn load(dict: Dict, parent: Affine) -> Option<Pattern> {
let mut funcs = FunctionCache::new();
let mut diags = Diagnostics::default();
Pattern::load(
&Object::Dict(dict),
parent,
None,
&NoResolve,
&mut funcs,
&Limits::default(),
&mut diags,
)
}
#[test]
fn an_unknown_pattern_type_yields_no_pattern() {
for kind in [0i64, 3, -1] {
let dict = Dict::from_pairs([(Name::from("PatternType"), Object::Int(kind))]);
assert!(load(dict, Affine::IDENTITY).is_none(), "type {kind}");
}
assert!(load(Dict::new(), Affine::IDENTITY).is_none());
}
#[test]
fn a_tiling_pattern_must_be_a_stream() {
let dict = Dict::from_pairs([(Name::from("PatternType"), Object::Int(1))]);
assert!(load(dict, Affine::IDENTITY).is_none());
}
#[test]
fn uncolored_fallbacks_differ_by_paint_type() {
let no_base = ColorSpace::Pattern(Box::default());
let grey = uncolored_pattern_rgb(&no_base, &[0.5], true);
assert!((grey.r - 191.0 / 255.0).abs() < 1e-5);
let white = uncolored_pattern_rgb(&no_base, &[0.5], false);
assert!((white.r - 1.0).abs() < 1e-6);
let with_base = ColorSpace::Pattern(Box::new(PatternSpace {
base: Some(Box::new(ColorSpace::DeviceGray)),
}));
let resolved = uncolored_pattern_rgb(&with_base, &[0.25], true);
assert!((resolved.r - 0.25).abs() < 1e-6);
}
}