use std::collections::{BTreeMap, BTreeSet};
use std::str;
use oxideav_core::vector::{
DashPattern, FillRule, GradientStop, Group, LineCap, LineJoin, LinearGradient, Node, Paint,
Path, PathCommand, PathNode, Point, RadialGradient, Rgba, SpreadMethod, Stroke, Transform2D,
};
use crate::error::PdfError;
use crate::objects::{Dict, Object};
use crate::reader::inline_images::{find_inline_image_ei, parse_one_inline_image, PdfInlineImage};
pub fn parse_content_stream(input: &[u8]) -> Result<Group, PdfError> {
let mut state = State::new(None, None, None, None, None);
state.parse(input)?;
Ok(state.finish().root)
}
pub fn parse_content_stream_with_resources(
input: &[u8],
ext_gstate: Option<&Dict>,
) -> Result<Group, PdfError> {
let mut state = State::new(ext_gstate, None, None, None, None);
state.parse(input)?;
Ok(state.finish().root)
}
pub fn parse_content_stream_full(
input: &[u8],
ext_gstate: Option<&Dict>,
font_resources: Option<&Dict>,
) -> Result<ParsedContent, PdfError> {
parse_content_stream_full_with_shading(input, ext_gstate, font_resources, None)
}
pub fn parse_content_stream_full_with_shading(
input: &[u8],
ext_gstate: Option<&Dict>,
font_resources: Option<&Dict>,
shading_resources: Option<&Dict>,
) -> Result<ParsedContent, PdfError> {
parse_content_stream_full_with_color_space(
input,
ext_gstate,
font_resources,
shading_resources,
None,
)
}
pub fn parse_content_stream_full_with_color_space(
input: &[u8],
ext_gstate: Option<&Dict>,
font_resources: Option<&Dict>,
shading_resources: Option<&Dict>,
color_space_resources: Option<&Dict>,
) -> Result<ParsedContent, PdfError> {
parse_content_stream_full_with_properties(
input,
ext_gstate,
font_resources,
shading_resources,
color_space_resources,
None,
)
}
pub fn parse_content_stream_full_with_properties(
input: &[u8],
ext_gstate: Option<&Dict>,
font_resources: Option<&Dict>,
shading_resources: Option<&Dict>,
color_space_resources: Option<&Dict>,
properties_resources: Option<&Dict>,
) -> Result<ParsedContent, PdfError> {
let mut state = State::new(
ext_gstate,
font_resources,
shading_resources,
color_space_resources,
properties_resources,
);
state.parse(input)?;
Ok(state.finish())
}
#[allow(clippy::too_many_arguments)]
pub fn parse_content_stream_full_with_xobjects(
input: &[u8],
ext_gstate: Option<&Dict>,
font_resources: Option<&Dict>,
shading_resources: Option<&Dict>,
color_space_resources: Option<&Dict>,
properties_resources: Option<&Dict>,
xobject_forms: Option<&BTreeMap<String, Group>>,
) -> Result<ParsedContent, PdfError> {
let mut state = State::new(
ext_gstate,
font_resources,
shading_resources,
color_space_resources,
properties_resources,
)
.with_xobject_forms(xobject_forms);
state.parse(input)?;
Ok(state.finish())
}
#[allow(clippy::too_many_arguments)]
pub fn parse_content_stream_full_with_patterns(
input: &[u8],
ext_gstate: Option<&Dict>,
font_resources: Option<&Dict>,
shading_resources: Option<&Dict>,
color_space_resources: Option<&Dict>,
properties_resources: Option<&Dict>,
xobject_forms: Option<&BTreeMap<String, Group>>,
pattern_resources: Option<&Dict>,
) -> Result<ParsedContent, PdfError> {
let mut state = State::new(
ext_gstate,
font_resources,
shading_resources,
color_space_resources,
properties_resources,
)
.with_xobject_forms(xobject_forms)
.with_pattern_resources(pattern_resources);
state.parse(input)?;
Ok(state.finish())
}
#[allow(clippy::too_many_arguments)]
pub fn parse_content_stream_full_with_tiling(
input: &[u8],
ext_gstate: Option<&Dict>,
font_resources: Option<&Dict>,
shading_resources: Option<&Dict>,
color_space_resources: Option<&Dict>,
properties_resources: Option<&Dict>,
xobject_forms: Option<&BTreeMap<String, Group>>,
pattern_resources: Option<&Dict>,
tiling_patterns: Option<&BTreeMap<String, TilingPattern>>,
) -> Result<ParsedContent, PdfError> {
let mut state = State::new(
ext_gstate,
font_resources,
shading_resources,
color_space_resources,
properties_resources,
)
.with_xobject_forms(xobject_forms)
.with_pattern_resources(pattern_resources)
.with_tiling_patterns(tiling_patterns);
state.parse(input)?;
Ok(state.finish())
}
#[allow(clippy::too_many_arguments)]
pub fn parse_content_stream_full_with_type3(
input: &[u8],
ext_gstate: Option<&Dict>,
font_resources: Option<&Dict>,
shading_resources: Option<&Dict>,
color_space_resources: Option<&Dict>,
properties_resources: Option<&Dict>,
xobject_forms: Option<&BTreeMap<String, Group>>,
pattern_resources: Option<&Dict>,
tiling_patterns: Option<&BTreeMap<String, TilingPattern>>,
type3_fonts: Option<&BTreeMap<String, Type3Font>>,
) -> Result<ParsedContent, PdfError> {
let mut state = State::new(
ext_gstate,
font_resources,
shading_resources,
color_space_resources,
properties_resources,
)
.with_xobject_forms(xobject_forms)
.with_pattern_resources(pattern_resources)
.with_tiling_patterns(tiling_patterns)
.with_type3_fonts(type3_fonts);
state.parse(input)?;
Ok(state.finish())
}
#[derive(Clone, Debug, Default)]
pub struct ParsedContent {
pub root: Group,
pub text_shows: Vec<ContentTextShow>,
pub shadings: Vec<ContentShading>,
pub marked_content: Vec<ContentMarkedContent>,
pub inline_images: Vec<ContentInlineImage>,
}
#[derive(Clone, Debug)]
pub struct ContentTextShow {
pub font_name: String,
pub font_size: f32,
pub font_dict: Option<Dict>,
pub bytes: Vec<u8>,
pub position: (f32, f32),
pub operator: TextShowOp,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextShowOp {
Tj,
TJ,
SingleQuote,
DoubleQuote,
}
#[derive(Clone, Debug)]
pub struct ContentInlineImage {
pub image: PdfInlineImage,
pub ctm: Transform2D,
pub clip: Option<Path>,
}
#[derive(Clone, Debug)]
pub struct ContentShading {
pub name: String,
pub shading_dict: Option<Dict>,
pub ctm: Transform2D,
pub clip: Option<Path>,
pub mesh: Option<MeshShading>,
pub gradient: Option<ShadingGradient>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ShadingGradient {
Axial {
coords: [f32; 4],
extend: [bool; 2],
stops: Vec<Rgba>,
},
Radial {
coords: [f32; 6],
extend: [bool; 2],
stops: Vec<Rgba>,
},
FunctionBased {
domain: [f32; 4],
matrix: Transform2D,
grid: (usize, usize),
samples: Vec<Rgba>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum MeshShading {
Triangles(Vec<MeshTriangle>),
Patches(Vec<MeshPatch>),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MeshTriangle {
pub vertices: [MeshVertex; 3],
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MeshVertex {
pub point: Point,
pub color: Rgba,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MeshPatch {
pub control_points: [[Point; 4]; 4],
pub corner_colors: [Rgba; 4],
}
#[derive(Clone, Debug)]
pub struct ContentMarkedContent {
pub operator: MarkedContentOp,
pub tag: String,
pub properties: Option<Dict>,
pub depth: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MarkedContentOp {
Mp,
Dp,
Bmc,
Bdc,
Emc,
}
struct State<'a> {
operands: Vec<Operand>,
stack: Vec<Frame>,
current_path: Option<Path>,
current_point: Point,
fill_paint: Option<Paint>,
stroke_paint: Option<Paint>,
fill_cs: ColorSpaceKind,
stroke_cs: ColorSpaceKind,
stroke_width: f32,
line_cap: LineCap,
line_join: LineJoin,
miter_limit: f32,
dash: Option<DashPattern>,
fill_alpha: f32,
stroke_alpha: f32,
ext_gstate: Option<&'a Dict>,
font_resources: Option<&'a Dict>,
shading_resources: Option<&'a Dict>,
color_space_resources: Option<&'a Dict>,
properties_resources: Option<&'a Dict>,
mc_depth: u32,
marked_content: Vec<ContentMarkedContent>,
current_font: Option<(String, f32)>,
text_matrix: Transform2D,
text_line_matrix: Transform2D,
text_leading: f32,
char_spacing: f32,
word_spacing: f32,
horiz_scale: f32,
in_text_object: bool,
text_shows: Vec<ContentTextShow>,
shadings: Vec<ContentShading>,
inline_images: Vec<ContentInlineImage>,
xobject_forms: Option<&'a BTreeMap<String, Group>>,
pattern_resources: Option<&'a Dict>,
tiling_patterns: Option<&'a BTreeMap<String, TilingPattern>>,
fill_tiling: Option<String>,
fill_tiling_color: Option<Rgba>,
stroke_tiling: Option<String>,
type3_fonts: Option<&'a BTreeMap<String, Type3Font>>,
text_render_mode: i64,
text_rise: f32,
type3_depth: u32,
}
#[derive(Clone, Debug)]
pub struct TilingPattern {
pub cell: Group,
pub bbox: [f32; 4],
pub xstep: f32,
pub ystep: f32,
pub matrix: Transform2D,
pub paint_type: i64,
}
#[derive(Clone, Debug)]
pub struct Type3Font {
pub font_matrix: Transform2D,
pub encoding: BTreeMap<u8, String>,
pub glyphs: BTreeMap<String, Group>,
pub shape_only: BTreeSet<String>,
}
struct Frame {
transform: Transform2D,
children: Vec<Node>,
clip: Option<Path>,
}
#[derive(Clone, Debug)]
enum Operand {
Number(f32),
Array(Vec<ArrayElem>),
Name(String),
String(Vec<u8>),
Dict(Dict),
}
#[derive(Clone, Debug)]
enum ArrayElem {
Number(f32),
String(Vec<u8>),
}
enum TjElem {
Str(Vec<u8>),
Kern(f32),
}
#[derive(Clone, Debug, PartialEq)]
enum PdfFunction {
Exponential {
domain: [f32; 2],
range: Option<Vec<f32>>,
c0: Vec<f32>,
c1: Vec<f32>,
n: f32,
},
Stitching {
domain: [f32; 2],
range: Option<Vec<f32>>,
functions: Vec<PdfFunction>,
bounds: Vec<f32>,
encode: Vec<f32>,
},
Sampled {
domain: Vec<f32>,
range: Vec<f32>,
size: Vec<usize>,
n: usize,
encode: Vec<f32>,
decode: Vec<f32>,
samples: Vec<f32>,
order: u8,
},
Calculator {
domain: Vec<f32>,
range: Vec<f32>,
program: Vec<PsToken>,
},
}
#[derive(Clone, Debug, PartialEq)]
enum PsToken {
Number(f32),
Bool(bool),
Op(PsOp),
Block(Vec<PsToken>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PsOp {
Abs,
Add,
Atan,
Ceiling,
Cos,
Cvi,
Cvr,
Div,
Exp,
Floor,
Idiv,
Ln,
Log,
Mod,
Mul,
Neg,
Round,
Sin,
Sqrt,
Sub,
Truncate,
And,
Bitshift,
Eq,
Ge,
Gt,
Le,
Lt,
Ne,
Not,
Or,
Xor,
If,
Ifelse,
Copy,
Dup,
Exch,
Index,
Pop,
Roll,
}
impl PsOp {
fn from_keyword(kw: &str) -> Option<PsOp> {
Some(match kw {
"abs" => PsOp::Abs,
"add" => PsOp::Add,
"atan" => PsOp::Atan,
"ceiling" => PsOp::Ceiling,
"cos" => PsOp::Cos,
"cvi" => PsOp::Cvi,
"cvr" => PsOp::Cvr,
"div" => PsOp::Div,
"exp" => PsOp::Exp,
"floor" => PsOp::Floor,
"idiv" => PsOp::Idiv,
"ln" => PsOp::Ln,
"log" => PsOp::Log,
"mod" => PsOp::Mod,
"mul" => PsOp::Mul,
"neg" => PsOp::Neg,
"round" => PsOp::Round,
"sin" => PsOp::Sin,
"sqrt" => PsOp::Sqrt,
"sub" => PsOp::Sub,
"truncate" => PsOp::Truncate,
"and" => PsOp::And,
"bitshift" => PsOp::Bitshift,
"eq" => PsOp::Eq,
"ge" => PsOp::Ge,
"gt" => PsOp::Gt,
"le" => PsOp::Le,
"lt" => PsOp::Lt,
"ne" => PsOp::Ne,
"not" => PsOp::Not,
"or" => PsOp::Or,
"xor" => PsOp::Xor,
"if" => PsOp::If,
"ifelse" => PsOp::Ifelse,
"copy" => PsOp::Copy,
"dup" => PsOp::Dup,
"exch" => PsOp::Exch,
"index" => PsOp::Index,
"pop" => PsOp::Pop,
"roll" => PsOp::Roll,
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum PsValue {
Num(f32),
Bool(bool),
}
impl PdfFunction {
fn parse(obj: &Object) -> Option<PdfFunction> {
let Object::Dict(dict) = obj else {
return None;
};
let get = |key: &str| {
dict.entries()
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v)
};
let range = get("Range").and_then(read_num_array);
match get("FunctionType").and_then(number_as_i64) {
Some(0) => {
let domain = get("Domain").and_then(read_num_array)?;
if domain.is_empty() || domain.len() % 2 != 0 {
return None;
}
let range = range?;
if range.is_empty() || range.len() % 2 != 0 {
return None;
}
let n = range.len() / 2;
let order = match get("Order").and_then(number_as_i64) {
Some(1) | None => 1u8,
Some(3) => 3u8,
Some(_) => return None,
};
let size_arr = get("Size").and_then(read_num_array)?;
let m = domain.len() / 2;
if size_arr.len() != m {
return None;
}
let mut size = Vec::with_capacity(m);
for s in &size_arr {
if !s.is_finite() || *s < 1.0 {
return None;
}
size.push(*s as usize);
}
let bps = get("BitsPerSample").and_then(number_as_i64)?;
if !matches!(bps, 1 | 2 | 4 | 8 | 12 | 16 | 24 | 32) {
return None;
}
let bps = bps as u32;
let encode = match get("Encode").and_then(read_num_array) {
Some(e) if e.len() == 2 * m => e,
Some(_) => return None,
None => {
let mut e = Vec::with_capacity(2 * m);
for &sz in &size {
e.push(0.0);
e.push((sz as f32) - 1.0);
}
e
}
};
let decode = get("Decode")
.and_then(read_num_array)
.unwrap_or_else(|| range.clone());
if decode.len() != 2 * n {
return None;
}
let mut total: usize = 1;
for &sz in &size {
total = total.checked_mul(sz)?;
}
let count = total.checked_mul(n)?;
let raw = match get("__Samples") {
Some(Object::HexString(bytes)) => bytes.as_slice(),
_ => return None,
};
let samples = unpack_samples(raw, bps, count)?;
Some(PdfFunction::Sampled {
domain,
range,
size,
n,
encode,
decode,
samples,
order,
})
}
Some(2) => {
let domain = get("Domain").and_then(read_num_pair)?;
let c0 = get("C0")
.and_then(read_num_array)
.unwrap_or_else(|| vec![0.0]);
let c1 = get("C1")
.and_then(read_num_array)
.unwrap_or_else(|| vec![1.0]);
let n = get("N").and_then(number_as_f32)?;
if c0.len() != c1.len() || c0.is_empty() {
return None;
}
Some(PdfFunction::Exponential {
domain,
range,
c0,
c1,
n,
})
}
Some(3) => {
let domain = get("Domain").and_then(read_num_pair)?;
let Some(Object::Array(fs)) = get("Functions") else {
return None;
};
let functions: Vec<PdfFunction> =
fs.iter().map(PdfFunction::parse).collect::<Option<_>>()?;
if functions.is_empty() {
return None;
}
let bounds = get("Bounds").and_then(read_num_array).unwrap_or_default();
let encode = get("Encode").and_then(read_num_array)?;
if bounds.len() + 1 != functions.len() || encode.len() != 2 * functions.len() {
return None;
}
Some(PdfFunction::Stitching {
domain,
range,
functions,
bounds,
encode,
})
}
Some(4) => {
let domain = get("Domain").and_then(read_num_array)?;
if domain.is_empty() || domain.len() % 2 != 0 {
return None;
}
let range = range?;
if range.is_empty() || range.len() % 2 != 0 {
return None;
}
let src = match get("__Program") {
Some(Object::HexString(bytes)) => bytes.as_slice(),
_ => return None,
};
let program = parse_ps_program(src)?;
Some(PdfFunction::Calculator {
domain,
range,
program,
})
}
_ => None,
}
}
fn eval(&self, x: f32) -> Vec<f32> {
self.eval_n(&[x])
}
fn eval_n(&self, inputs: &[f32]) -> Vec<f32> {
let first = inputs.first().copied().unwrap_or(0.0);
match self {
PdfFunction::Exponential {
domain,
range,
c0,
c1,
n,
} => {
let xc = first.clamp(domain[0], domain[1]);
let xn = xc.powf(*n);
let mut out: Vec<f32> = c0
.iter()
.zip(c1.iter())
.map(|(&a, &b)| a + xn * (b - a))
.collect();
clip_to_range(&mut out, range.as_deref());
out
}
PdfFunction::Stitching {
domain,
range,
functions,
bounds,
encode,
} => {
let xc = first.clamp(domain[0], domain[1]);
let k = functions.len();
let mut i = 0;
while i < bounds.len() && xc >= bounds[i] {
i += 1;
}
let lo = if i == 0 { domain[0] } else { bounds[i - 1] };
let hi = if i == k - 1 { domain[1] } else { bounds[i] };
let xprime = if (hi - lo).abs() < f32::EPSILON {
encode[2 * i]
} else {
interpolate(xc, lo, hi, encode[2 * i], encode[2 * i + 1])
};
let mut out = functions[i].eval(xprime);
clip_to_range(&mut out, range.as_deref());
out
}
PdfFunction::Sampled {
domain,
range,
size,
n,
encode,
decode,
samples,
order,
} => eval_sampled(
inputs, domain, range, size, *n, encode, decode, samples, *order,
),
PdfFunction::Calculator {
domain,
range,
program,
} => {
let m = domain.len() / 2;
let n = range.len() / 2;
let mut stack: Vec<PsValue> = Vec::with_capacity(m);
for i in 0..m {
let xi = inputs.get(i).copied().unwrap_or(0.0);
stack.push(PsValue::Num(xi.clamp(domain[2 * i], domain[2 * i + 1])));
}
if exec_ps(program, &mut stack).is_err() {
return vec![0.0; n];
}
if stack.len() != n {
return vec![0.0; n];
}
let mut out = Vec::with_capacity(n);
for v in &stack {
match v {
PsValue::Num(f) => out.push(*f),
PsValue::Bool(_) => return vec![0.0; n],
}
}
clip_to_range(&mut out, Some(range));
out
}
}
}
fn input_arity(&self) -> usize {
match self {
PdfFunction::Exponential { .. } | PdfFunction::Stitching { .. } => 1,
PdfFunction::Sampled { size, .. } => size.len(),
PdfFunction::Calculator { domain, .. } => domain.len() / 2,
}
}
fn output_arity(&self) -> Option<usize> {
match self {
PdfFunction::Sampled { n, .. } => Some(*n),
PdfFunction::Exponential { c0, .. } => Some(c0.len()),
PdfFunction::Calculator { range, .. } => Some(range.len() / 2),
PdfFunction::Stitching { .. } => None,
}
}
}
const PS_STACK_LIMIT: usize = 100;
fn parse_ps_program(src: &[u8]) -> Option<Vec<PsToken>> {
let mut words: Vec<&[u8]> = Vec::new();
let mut start: Option<usize> = None;
for (i, &b) in src.iter().enumerate() {
if b == b'{' || b == b'}' {
if let Some(s) = start.take() {
words.push(&src[s..i]);
}
words.push(&src[i..i + 1]);
} else if b.is_ascii_whitespace() {
if let Some(s) = start.take() {
words.push(&src[s..i]);
}
} else if start.is_none() {
start = Some(i);
}
}
if let Some(s) = start {
words.push(&src[s..]);
}
let mut iter = words.into_iter().peekable();
if iter.next() != Some(b"{") {
return None;
}
let (body, closed) = parse_ps_block(&mut iter)?;
if !closed || iter.next().is_some() {
return None;
}
Some(body)
}
fn parse_ps_block<'a, I>(iter: &mut std::iter::Peekable<I>) -> Option<(Vec<PsToken>, bool)>
where
I: Iterator<Item = &'a [u8]>,
{
let mut tokens = Vec::new();
while let Some(w) = iter.next() {
match w {
b"}" => return Some((tokens, true)),
b"{" => {
let (inner, closed) = parse_ps_block(iter)?;
if !closed {
return None;
}
tokens.push(PsToken::Block(inner));
}
b"true" => tokens.push(PsToken::Bool(true)),
b"false" => tokens.push(PsToken::Bool(false)),
other => {
let text = str::from_utf8(other).ok()?;
if let Ok(num) = text.parse::<f32>() {
tokens.push(PsToken::Number(num));
} else if let Some(op) = PsOp::from_keyword(text) {
tokens.push(PsToken::Op(op));
} else {
return None;
}
}
}
}
Some((tokens, false))
}
fn exec_ps(tokens: &[PsToken], stack: &mut Vec<PsValue>) -> Result<(), ()> {
let mut i = 0;
while i < tokens.len() {
match &tokens[i] {
PsToken::Number(f) => push(stack, PsValue::Num(*f))?,
PsToken::Bool(b) => push(stack, PsValue::Bool(*b))?,
PsToken::Block(_) => {
let proc1 = match &tokens[i] {
PsToken::Block(b) => b,
_ => unreachable!(),
};
match tokens.get(i + 1) {
Some(PsToken::Op(PsOp::If)) => {
let cond = pop_bool(stack)?;
if cond {
exec_ps(proc1, stack)?;
}
i += 2;
continue;
}
Some(PsToken::Block(proc2)) => {
if tokens.get(i + 2) != Some(&PsToken::Op(PsOp::Ifelse)) {
return Err(());
}
let cond = pop_bool(stack)?;
if cond {
exec_ps(proc1, stack)?;
} else {
exec_ps(proc2, stack)?;
}
i += 3;
continue;
}
_ => return Err(()),
}
}
PsToken::Op(op) => exec_ps_op(*op, stack)?,
}
i += 1;
}
Ok(())
}
fn push(stack: &mut Vec<PsValue>, v: PsValue) -> Result<(), ()> {
if stack.len() >= PS_STACK_LIMIT {
return Err(());
}
stack.push(v);
Ok(())
}
fn pop_num(stack: &mut Vec<PsValue>) -> Result<f32, ()> {
match stack.pop() {
Some(PsValue::Num(f)) => Ok(f),
_ => Err(()),
}
}
fn pop_bool(stack: &mut Vec<PsValue>) -> Result<bool, ()> {
match stack.pop() {
Some(PsValue::Bool(b)) => Ok(b),
_ => Err(()),
}
}
fn pop_int(stack: &mut Vec<PsValue>) -> Result<i32, ()> {
let f = pop_num(stack)?;
if !f.is_finite() || f.fract() != 0.0 || f < i32::MIN as f32 || f > i32::MAX as f32 {
return Err(());
}
Ok(f as i32)
}
fn exec_ps_op(op: PsOp, stack: &mut Vec<PsValue>) -> Result<(), ()> {
match op {
PsOp::Add => {
let (a, b) = (pop_num(stack)?, pop_num(stack)?);
push(stack, PsValue::Num(b + a))
}
PsOp::Sub => {
let (a, b) = (pop_num(stack)?, pop_num(stack)?);
push(stack, PsValue::Num(b - a))
}
PsOp::Mul => {
let (a, b) = (pop_num(stack)?, pop_num(stack)?);
push(stack, PsValue::Num(b * a))
}
PsOp::Div => {
let a = pop_num(stack)?;
let b = pop_num(stack)?;
if a == 0.0 {
return Err(()); }
push(stack, PsValue::Num(b / a))
}
PsOp::Idiv => {
let a = pop_int(stack)?;
let b = pop_int(stack)?;
if a == 0 {
return Err(());
}
push(stack, PsValue::Num((b / a) as f32))
}
PsOp::Mod => {
let a = pop_int(stack)?;
let b = pop_int(stack)?;
if a == 0 {
return Err(());
}
push(stack, PsValue::Num((b % a) as f32))
}
PsOp::Neg => {
let a = pop_num(stack)?;
push(stack, PsValue::Num(-a))
}
PsOp::Abs => {
let a = pop_num(stack)?;
push(stack, PsValue::Num(a.abs()))
}
PsOp::Ceiling => {
let a = pop_num(stack)?;
push(stack, PsValue::Num(a.ceil()))
}
PsOp::Floor => {
let a = pop_num(stack)?;
push(stack, PsValue::Num(a.floor()))
}
PsOp::Round => {
let a = pop_num(stack)?;
push(stack, PsValue::Num(a.round()))
}
PsOp::Truncate => {
let a = pop_num(stack)?;
push(stack, PsValue::Num(a.trunc()))
}
PsOp::Sqrt => {
let a = pop_num(stack)?;
if a < 0.0 {
return Err(()); }
push(stack, PsValue::Num(a.sqrt()))
}
PsOp::Sin => {
let a = pop_num(stack)?;
push(stack, PsValue::Num(a.to_radians().sin()))
}
PsOp::Cos => {
let a = pop_num(stack)?;
push(stack, PsValue::Num(a.to_radians().cos()))
}
PsOp::Atan => {
let den = pop_num(stack)?;
let num = pop_num(stack)?;
if num == 0.0 && den == 0.0 {
return Err(()); }
let mut deg = num.atan2(den).to_degrees();
if deg < 0.0 {
deg += 360.0;
}
push(stack, PsValue::Num(deg))
}
PsOp::Exp => {
let exponent = pop_num(stack)?;
let base = pop_num(stack)?;
let r = base.powf(exponent);
if !r.is_finite() {
return Err(());
}
push(stack, PsValue::Num(r))
}
PsOp::Ln => {
let a = pop_num(stack)?;
if a <= 0.0 {
return Err(());
}
push(stack, PsValue::Num(a.ln()))
}
PsOp::Log => {
let a = pop_num(stack)?;
if a <= 0.0 {
return Err(());
}
push(stack, PsValue::Num(a.log10()))
}
PsOp::Cvi => {
let a = pop_num(stack)?;
push(stack, PsValue::Num(a.trunc()))
}
PsOp::Cvr => {
let a = pop_num(stack)?;
push(stack, PsValue::Num(a))
}
PsOp::Eq => {
let (a, b) = (stack.pop().ok_or(())?, stack.pop().ok_or(())?);
push(stack, PsValue::Bool(b == a))
}
PsOp::Ne => {
let (a, b) = (stack.pop().ok_or(())?, stack.pop().ok_or(())?);
push(stack, PsValue::Bool(b != a))
}
PsOp::Gt => {
let (a, b) = (pop_num(stack)?, pop_num(stack)?);
push(stack, PsValue::Bool(b > a))
}
PsOp::Ge => {
let (a, b) = (pop_num(stack)?, pop_num(stack)?);
push(stack, PsValue::Bool(b >= a))
}
PsOp::Lt => {
let (a, b) = (pop_num(stack)?, pop_num(stack)?);
push(stack, PsValue::Bool(b < a))
}
PsOp::Le => {
let (a, b) = (pop_num(stack)?, pop_num(stack)?);
push(stack, PsValue::Bool(b <= a))
}
PsOp::And => bool_or_bitwise(stack, |x, y| x & y, |x, y| x && y),
PsOp::Or => bool_or_bitwise(stack, |x, y| x | y, |x, y| x || y),
PsOp::Xor => bool_or_bitwise(stack, |x, y| x ^ y, |x, y| x != y),
PsOp::Not => {
match stack.pop() {
Some(PsValue::Bool(b)) => push(stack, PsValue::Bool(!b)),
Some(PsValue::Num(f)) => push(stack, PsValue::Num(!integer_value(f)? as f32)),
_ => Err(()),
}
}
PsOp::Bitshift => {
let shift = pop_int(stack)?;
let v = pop_int(stack)?;
let r = if shift >= 0 {
if shift >= 32 {
0
} else {
v.wrapping_shl(shift as u32)
}
} else {
let s = (-shift) as u32;
if s >= 32 {
0
} else {
v >> s
}
};
push(stack, PsValue::Num(r as f32))
}
PsOp::Pop => {
stack.pop().ok_or(())?;
Ok(())
}
PsOp::Exch => {
let len = stack.len();
if len < 2 {
return Err(());
}
stack.swap(len - 1, len - 2);
Ok(())
}
PsOp::Dup => {
let top = *stack.last().ok_or(())?;
push(stack, top)
}
PsOp::Copy => {
let n = pop_int(stack)?;
if n < 0 {
return Err(());
}
let n = n as usize;
let len = stack.len();
if n > len {
return Err(());
}
if stack.len() + n > PS_STACK_LIMIT {
return Err(());
}
for k in 0..n {
stack.push(stack[len - n + k]);
}
Ok(())
}
PsOp::Index => {
let n = pop_int(stack)?;
if n < 0 {
return Err(());
}
let n = n as usize;
let len = stack.len();
if n >= len {
return Err(());
}
push(stack, stack[len - 1 - n])
}
PsOp::Roll => {
let j = pop_int(stack)?;
let n = pop_int(stack)?;
if n < 0 {
return Err(());
}
let n = n as usize;
let len = stack.len();
if n > len {
return Err(());
}
if n > 0 {
let base = len - n;
let slice = &mut stack[base..];
let k = j.rem_euclid(n as i32) as usize;
slice.rotate_right(k);
}
Ok(())
}
PsOp::If | PsOp::Ifelse => Err(()),
}
}
fn integer_value(f: f32) -> Result<i32, ()> {
if !f.is_finite() || f.fract() != 0.0 || f < i32::MIN as f32 || f > i32::MAX as f32 {
return Err(());
}
Ok(f as i32)
}
fn bool_or_bitwise(
stack: &mut Vec<PsValue>,
bitwise: fn(i32, i32) -> i32,
logical: fn(bool, bool) -> bool,
) -> Result<(), ()> {
let a = stack.pop().ok_or(())?;
let b = stack.pop().ok_or(())?;
match (b, a) {
(PsValue::Bool(x), PsValue::Bool(y)) => push(stack, PsValue::Bool(logical(x, y))),
(PsValue::Num(x), PsValue::Num(y)) => {
let xi = integer_value(x)?;
let yi = integer_value(y)?;
push(stack, PsValue::Num(bitwise(xi, yi) as f32))
}
_ => Err(()),
}
}
fn interpolate(x: f32, xmin: f32, xmax: f32, ymin: f32, ymax: f32) -> f32 {
if (xmax - xmin).abs() < f32::EPSILON {
return ymin;
}
ymin + (x - xmin) * (ymax - ymin) / (xmax - xmin)
}
fn clip_to_range(out: &mut [f32], range: Option<&[f32]>) {
if let Some(r) = range {
for (j, v) in out.iter_mut().enumerate() {
if let (Some(&lo), Some(&hi)) = (r.get(2 * j), r.get(2 * j + 1)) {
*v = v.clamp(lo, hi);
}
}
}
}
fn read_num_pair(obj: &Object) -> Option<[f32; 2]> {
let Object::Array(items) = obj else {
return None;
};
if items.len() != 2 {
return None;
}
Some([number_as_f32(&items[0])?, number_as_f32(&items[1])?])
}
fn unpack_samples(raw: &[u8], bps: u32, count: usize) -> Option<Vec<f32>> {
let total_bits = (count as u64).checked_mul(bps as u64)?;
if (raw.len() as u64) * 8 < total_bits {
return None;
}
let max_code = ((1u64 << bps) - 1) as f32;
let mut out = Vec::with_capacity(count);
let mut bit_pos: u64 = 0;
for _ in 0..count {
let mut code: u64 = 0;
for _ in 0..bps {
let byte = raw[(bit_pos / 8) as usize];
let bit = (byte >> (7 - (bit_pos % 8) as u32)) & 1;
code = (code << 1) | (bit as u64);
bit_pos += 1;
}
out.push((code as f32) / max_code);
}
Some(out)
}
fn cubic_weights(t: f32) -> [f32; 4] {
let t2 = t * t;
let t3 = t2 * t;
[
-0.5 * t3 + t2 - 0.5 * t,
1.5 * t3 - 2.5 * t2 + 1.0,
-1.5 * t3 + 2.0 * t2 + 0.5 * t,
0.5 * t3 - 0.5 * t2,
]
}
type AxisTaps = smallvec_like::Taps;
mod smallvec_like {
#[derive(Clone, Copy)]
pub(super) struct Taps {
pub(super) items: [(usize, f32); 4],
pub(super) len: usize,
}
impl Taps {
pub(super) fn new() -> Self {
Taps {
items: [(0, 0.0); 4],
len: 0,
}
}
pub(super) fn push(&mut self, idx: usize, w: f32) {
self.items[self.len] = (idx, w);
self.len += 1;
}
pub(super) fn as_slice(&self) -> &[(usize, f32)] {
&self.items[..self.len]
}
}
}
#[allow(clippy::too_many_arguments)]
fn eval_sampled(
inputs: &[f32],
domain: &[f32],
range: &[f32],
size: &[usize],
n: usize,
encode: &[f32],
decode: &[f32],
samples: &[f32],
order: u8,
) -> Vec<f32> {
let m = size.len();
let mut taps: Vec<AxisTaps> = Vec::with_capacity(m);
for i in 0..m {
let xi = inputs.get(i).copied().unwrap_or(0.0);
let xc = xi.clamp(domain[2 * i], domain[2 * i + 1]);
let e = interpolate(
xc,
domain[2 * i],
domain[2 * i + 1],
encode[2 * i],
encode[2 * i + 1],
);
let last = size[i] - 1;
let e = e.clamp(0.0, last as f32);
let i0 = e.floor() as usize;
let frac = e - (i0 as f32);
let mut t = AxisTaps::new();
if order == 3 && size[i] >= 4 {
let w = cubic_weights(frac);
let lo = i0 as isize - 1;
for (k, &wk) in w.iter().enumerate() {
let idx = (lo + k as isize).clamp(0, last as isize) as usize;
if wk != 0.0 {
t.push(idx, wk);
}
}
} else {
let up = (i0 + 1).min(last);
t.push(i0, 1.0 - frac);
if frac != 0.0 && up != i0 {
t.push(up, frac);
}
}
taps.push(t);
}
let mut out = vec![0.0f32; n];
let mut idx_in_axis = vec![0usize; m];
loop {
let mut weight = 1.0f32;
let mut flat = 0usize;
let mut stride = 1usize;
for i in 0..m {
let (idx, w) = taps[i].as_slice()[idx_in_axis[i]];
weight *= w;
flat += idx * stride;
stride *= size[i];
}
if weight != 0.0 {
let off = flat * n;
for (j, acc) in out.iter_mut().enumerate() {
*acc += weight * samples[off + j];
}
}
let mut axis = 0;
loop {
if axis == m {
idx_in_axis.clear();
break;
}
idx_in_axis[axis] += 1;
if idx_in_axis[axis] < taps[axis].as_slice().len() {
break;
}
idx_in_axis[axis] = 0;
axis += 1;
}
if idx_in_axis.is_empty() {
break;
}
}
for (j, v) in out.iter_mut().enumerate() {
*v = interpolate(*v, 0.0, 1.0, decode[2 * j], decode[2 * j + 1]);
}
clip_to_range(&mut out, Some(range));
out
}
fn read_num_array(obj: &Object) -> Option<Vec<f32>> {
let Object::Array(items) = obj else {
return None;
};
items.iter().map(number_as_f32).collect()
}
#[derive(Clone, Debug, PartialEq)]
enum ColorSpaceKind {
DeviceGray,
DeviceRgb,
DeviceCmyk,
Indexed {
base: Box<ColorSpaceKind>,
hival: u32,
table: Vec<u8>,
},
Separation {
alt: Box<ColorSpaceKind>,
tint: PdfFunction,
none_colorant: bool,
},
DeviceN {
n_in: usize,
alt: Box<ColorSpaceKind>,
tint: PdfFunction,
all_none: bool,
},
CalGray { white: [f32; 3], gamma: f32 },
CalRgb { gamma: [f32; 3], matrix: [f32; 9] },
Lab { white: [f32; 3], range: [f32; 4] },
Unknown,
}
impl ColorSpaceKind {
fn from_name(name: &str) -> Self {
match name {
"DeviceGray" | "G" => ColorSpaceKind::DeviceGray,
"DeviceRGB" | "RGB" => ColorSpaceKind::DeviceRgb,
"DeviceCMYK" | "CMYK" => ColorSpaceKind::DeviceCmyk,
_ => ColorSpaceKind::Unknown,
}
}
fn components(&self) -> Option<usize> {
match self {
ColorSpaceKind::DeviceGray => Some(1),
ColorSpaceKind::DeviceRgb => Some(3),
ColorSpaceKind::DeviceCmyk => Some(4),
ColorSpaceKind::Indexed { .. } => Some(1),
ColorSpaceKind::Separation { .. } => Some(1),
ColorSpaceKind::DeviceN { n_in, .. } => Some(*n_in),
ColorSpaceKind::CalGray { .. } => Some(1),
ColorSpaceKind::CalRgb { .. } | ColorSpaceKind::Lab { .. } => Some(3),
ColorSpaceKind::Unknown => None,
}
}
fn resolve_with_resources(name: &str, resources: Option<&Dict>) -> Self {
let direct = ColorSpaceKind::from_name(name);
if direct != ColorSpaceKind::Unknown {
return direct;
}
let Some(res) = resources else {
return ColorSpaceKind::Unknown;
};
match res.entries().iter().find(|(k, _)| k == name) {
Some((_, obj)) => color_space_from_object(obj),
None => ColorSpaceKind::Unknown,
}
}
}
fn color_space_from_object(obj: &Object) -> ColorSpaceKind {
match obj {
Object::Name(n) => ColorSpaceKind::from_name(n),
Object::Array(items) => match items.first() {
Some(Object::Name(family)) if family == "ICCBased" => icc_based_from_array(items),
Some(Object::Name(family)) if family == "Indexed" => indexed_from_array(items),
Some(Object::Name(family)) if family == "Separation" => separation_from_array(items),
Some(Object::Name(family)) if family == "DeviceN" => device_n_from_array(items),
Some(Object::Name(family)) if family == "CalGray" => cal_gray_from_array(items),
Some(Object::Name(family)) if family == "CalRGB" => cal_rgb_from_array(items),
Some(Object::Name(family)) if family == "Lab" => lab_from_array(items),
_ => ColorSpaceKind::Unknown,
},
_ => ColorSpaceKind::Unknown,
}
}
fn icc_based_from_array(items: &[Object]) -> ColorSpaceKind {
let Some(Object::Dict(dict)) = items.get(1) else {
return ColorSpaceKind::Unknown;
};
if let Some((_, alt)) = dict.entries().iter().find(|(k, _)| k == "Alternate") {
let resolved = color_space_from_object(alt);
if resolved != ColorSpaceKind::Unknown {
return resolved;
}
}
match dict.entries().iter().find(|(k, _)| k == "N") {
Some((_, Object::Integer(1))) => ColorSpaceKind::DeviceGray,
Some((_, Object::Integer(3))) => ColorSpaceKind::DeviceRgb,
Some((_, Object::Integer(4))) => ColorSpaceKind::DeviceCmyk,
_ => ColorSpaceKind::Unknown,
}
}
fn indexed_from_array(items: &[Object]) -> ColorSpaceKind {
if items.len() < 4 {
return ColorSpaceKind::Unknown;
}
let base = color_space_from_object(&items[1]);
if base.components().is_none()
|| matches!(
base,
ColorSpaceKind::Indexed { .. }
| ColorSpaceKind::Separation { .. }
| ColorSpaceKind::DeviceN { .. }
)
{
return ColorSpaceKind::Unknown;
}
let hival = match &items[2] {
Object::Integer(n) if *n >= 0 && *n <= 255 => *n as u32,
_ => return ColorSpaceKind::Unknown,
};
let table = match &items[3] {
Object::LiteralString(b) | Object::HexString(b) => b.clone(),
_ => return ColorSpaceKind::Unknown,
};
ColorSpaceKind::Indexed {
base: Box::new(base),
hival,
table,
}
}
fn separation_from_array(items: &[Object]) -> ColorSpaceKind {
if items.len() < 4 {
return ColorSpaceKind::Unknown;
}
let none_colorant = matches!(&items[1], Object::Name(n) if n == "None");
let alt = color_space_from_object(&items[2]);
if alt.components().is_none()
|| matches!(
alt,
ColorSpaceKind::Indexed { .. }
| ColorSpaceKind::Separation { .. }
| ColorSpaceKind::DeviceN { .. }
)
{
if none_colorant {
return ColorSpaceKind::Separation {
alt: Box::new(ColorSpaceKind::DeviceGray),
tint: PdfFunction::Exponential {
domain: [0.0, 1.0],
range: None,
c0: vec![0.0],
c1: vec![0.0],
n: 1.0,
},
none_colorant: true,
};
}
return ColorSpaceKind::Unknown;
}
let Some(tint) = PdfFunction::parse(&items[3]) else {
return ColorSpaceKind::Unknown;
};
ColorSpaceKind::Separation {
alt: Box::new(alt),
tint,
none_colorant,
}
}
fn device_n_from_array(items: &[Object]) -> ColorSpaceKind {
if items.len() < 4 {
return ColorSpaceKind::Unknown;
}
let Object::Array(name_objs) = &items[1] else {
return ColorSpaceKind::Unknown;
};
if name_objs.is_empty() {
return ColorSpaceKind::Unknown;
}
let mut all_none = true;
for nm in name_objs {
match nm {
Object::Name(n) => {
if n != "None" {
all_none = false;
}
}
_ => return ColorSpaceKind::Unknown,
}
}
let n_in = name_objs.len();
let alt = color_space_from_object(&items[2]);
let alt_comps = match &alt {
ColorSpaceKind::DeviceGray | ColorSpaceKind::CalGray { .. } => 1,
ColorSpaceKind::DeviceRgb | ColorSpaceKind::CalRgb { .. } | ColorSpaceKind::Lab { .. } => 3,
ColorSpaceKind::DeviceCmyk => 4,
_ => return ColorSpaceKind::Unknown,
};
if all_none {
return ColorSpaceKind::DeviceN {
n_in,
alt: Box::new(alt),
tint: PdfFunction::Exponential {
domain: [0.0, 1.0],
range: None,
c0: vec![0.0],
c1: vec![0.0],
n: 1.0,
},
all_none: true,
};
}
let Some(tint) = PdfFunction::parse(&items[3]) else {
return ColorSpaceKind::Unknown;
};
if tint.input_arity() != n_in || tint.output_arity() != Some(alt_comps) {
return ColorSpaceKind::Unknown;
}
ColorSpaceKind::DeviceN {
n_in,
alt: Box::new(alt),
tint,
all_none: false,
}
}
fn read_fixed_num_array<const N: usize>(dict: &Dict, key: &str) -> Option<[f32; N]> {
let (_, obj) = dict.entries().iter().find(|(k, _)| k == key)?;
let nums = read_num_array(obj)?;
if nums.len() != N {
return None;
}
let mut out = [0.0f32; N];
out.copy_from_slice(&nums);
Some(out)
}
fn valid_white_point(w: [f32; 3]) -> bool {
w[0] > 0.0 && w[2] > 0.0 && (w[1] - 1.0).abs() < 1e-4 && w.iter().all(|c| c.is_finite())
}
fn cal_gray_from_array(items: &[Object]) -> ColorSpaceKind {
let Some(Object::Dict(dict)) = items.get(1) else {
return ColorSpaceKind::Unknown;
};
let Some(white) = read_fixed_num_array::<3>(dict, "WhitePoint") else {
return ColorSpaceKind::Unknown;
};
if !valid_white_point(white) {
return ColorSpaceKind::Unknown;
}
let gamma = match dict.entries().iter().find(|(k, _)| k == "Gamma") {
Some((_, obj)) => match number_as_f32(obj) {
Some(g) if g > 0.0 && g.is_finite() => g,
_ => return ColorSpaceKind::Unknown,
},
None => 1.0,
};
ColorSpaceKind::CalGray { white, gamma }
}
fn cal_rgb_from_array(items: &[Object]) -> ColorSpaceKind {
let Some(Object::Dict(dict)) = items.get(1) else {
return ColorSpaceKind::Unknown;
};
let Some(white) = read_fixed_num_array::<3>(dict, "WhitePoint") else {
return ColorSpaceKind::Unknown;
};
if !valid_white_point(white) {
return ColorSpaceKind::Unknown;
}
let gamma = match dict.entries().iter().find(|(k, _)| k == "Gamma") {
Some(_) => match read_fixed_num_array::<3>(dict, "Gamma") {
Some(g) if g.iter().all(|x| *x > 0.0 && x.is_finite()) => g,
_ => return ColorSpaceKind::Unknown,
},
None => [1.0, 1.0, 1.0],
};
let matrix = match dict.entries().iter().find(|(k, _)| k == "Matrix") {
Some(_) => match read_fixed_num_array::<9>(dict, "Matrix") {
Some(m) if m.iter().all(|x| x.is_finite()) => m,
_ => return ColorSpaceKind::Unknown,
},
None => [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
};
ColorSpaceKind::CalRgb { gamma, matrix }
}
fn lab_from_array(items: &[Object]) -> ColorSpaceKind {
let Some(Object::Dict(dict)) = items.get(1) else {
return ColorSpaceKind::Unknown;
};
let Some(white) = read_fixed_num_array::<3>(dict, "WhitePoint") else {
return ColorSpaceKind::Unknown;
};
if !valid_white_point(white) {
return ColorSpaceKind::Unknown;
}
let range = match dict.entries().iter().find(|(k, _)| k == "Range") {
Some(_) => match read_fixed_num_array::<4>(dict, "Range") {
Some(r) if r.iter().all(|x| x.is_finite()) && r[0] <= r[1] && r[2] <= r[3] => r,
_ => return ColorSpaceKind::Unknown,
},
None => [-100.0, 100.0, -100.0, 100.0],
};
ColorSpaceKind::Lab { white, range }
}
impl<'a> State<'a> {
fn new(
ext_gstate: Option<&'a Dict>,
font_resources: Option<&'a Dict>,
shading_resources: Option<&'a Dict>,
color_space_resources: Option<&'a Dict>,
properties_resources: Option<&'a Dict>,
) -> Self {
Self {
operands: Vec::new(),
stack: vec![Frame::new()],
current_path: None,
current_point: Point::default(),
fill_paint: None,
stroke_paint: None,
fill_cs: ColorSpaceKind::DeviceGray,
stroke_cs: ColorSpaceKind::DeviceGray,
stroke_width: 1.0,
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
miter_limit: 10.0,
dash: None,
fill_alpha: 1.0,
stroke_alpha: 1.0,
ext_gstate,
font_resources,
shading_resources,
color_space_resources,
properties_resources,
mc_depth: 0,
marked_content: Vec::new(),
current_font: None,
text_matrix: Transform2D::identity(),
text_line_matrix: Transform2D::identity(),
text_leading: 0.0,
char_spacing: 0.0,
word_spacing: 0.0,
horiz_scale: 1.0,
in_text_object: false,
text_shows: Vec::new(),
shadings: Vec::new(),
inline_images: Vec::new(),
xobject_forms: None,
pattern_resources: None,
tiling_patterns: None,
fill_tiling: None,
fill_tiling_color: None,
stroke_tiling: None,
type3_fonts: None,
text_render_mode: 0,
text_rise: 0.0,
type3_depth: 0,
}
}
fn with_xobject_forms(mut self, forms: Option<&'a BTreeMap<String, Group>>) -> Self {
self.xobject_forms = forms;
self
}
fn with_pattern_resources(mut self, patterns: Option<&'a Dict>) -> Self {
self.pattern_resources = patterns;
self
}
fn with_tiling_patterns(
mut self,
patterns: Option<&'a BTreeMap<String, TilingPattern>>,
) -> Self {
self.tiling_patterns = patterns;
self
}
fn with_type3_fonts(mut self, fonts: Option<&'a BTreeMap<String, Type3Font>>) -> Self {
self.type3_fonts = fonts;
self
}
fn finish(mut self) -> ParsedContent {
while self.stack.len() > 1 {
self.pop_q();
}
let root = self.stack.pop().expect("root frame present");
ParsedContent {
root: Group {
transform: root.transform,
opacity: 1.0,
clip: root.clip,
children: root.children,
..Group::default()
},
text_shows: self.text_shows,
shadings: self.shadings,
marked_content: self.marked_content,
inline_images: self.inline_images,
}
}
fn current(&mut self) -> &mut Frame {
self.stack.last_mut().expect("at least the root frame")
}
fn push_q(&mut self) {
self.stack.push(Frame::new());
}
fn pop_q(&mut self) {
if self.stack.len() <= 1 {
return;
}
let frame = self.stack.pop().unwrap();
if frame.is_effectively_empty() {
return;
}
let g = Group {
transform: frame.transform,
opacity: 1.0,
clip: frame.clip,
children: frame.children,
..Group::default()
};
self.current().children.push(Node::Group(g));
}
fn dispatch(&mut self, op: &[u8]) -> Result<(), PdfError> {
match op {
b"q" => {
self.push_q();
}
b"Q" => {
self.pop_q();
}
b"cm" => {
let nums = self.take_numbers(6)?;
let t = Transform2D {
a: nums[0],
b: nums[1],
c: nums[2],
d: nums[3],
e: nums[4],
f: nums[5],
};
let frame = self.current();
frame.transform = compose(frame.transform, t);
}
b"gs" => {
let name = match self.operands.last() {
Some(Operand::Name(n)) => Some(n.clone()),
_ => None,
};
self.operands.clear();
if let (Some(name), Some(ext_gstate)) = (name, self.ext_gstate) {
if let Some(dict) = lookup_dict(ext_gstate, &name) {
self.apply_ext_gstate(dict);
}
}
}
b"m" => {
let p = self.take_point()?;
let path = self.path_mut();
path.commands.push(PathCommand::MoveTo(p));
self.current_point = p;
}
b"l" => {
let p = self.take_point()?;
let path = self.path_mut();
path.commands.push(PathCommand::LineTo(p));
self.current_point = p;
}
b"c" => {
let nums = self.take_numbers(6)?;
let c1 = Point::new(nums[0], nums[1]);
let c2 = Point::new(nums[2], nums[3]);
let end = Point::new(nums[4], nums[5]);
let path = self.path_mut();
path.commands
.push(PathCommand::CubicCurveTo { c1, c2, end });
self.current_point = end;
}
b"v" => {
let nums = self.take_numbers(4)?;
let c1 = self.current_point;
let c2 = Point::new(nums[0], nums[1]);
let end = Point::new(nums[2], nums[3]);
let path = self.path_mut();
path.commands
.push(PathCommand::CubicCurveTo { c1, c2, end });
self.current_point = end;
}
b"y" => {
let nums = self.take_numbers(4)?;
let c1 = Point::new(nums[0], nums[1]);
let end = Point::new(nums[2], nums[3]);
let c2 = end;
let path = self.path_mut();
path.commands
.push(PathCommand::CubicCurveTo { c1, c2, end });
self.current_point = end;
}
b"re" => {
let nums = self.take_numbers(4)?;
let (x, y, w, h) = (nums[0], nums[1], nums[2], nums[3]);
let path = self.path_mut();
path.commands.push(PathCommand::MoveTo(Point::new(x, y)));
path.commands
.push(PathCommand::LineTo(Point::new(x + w, y)));
path.commands
.push(PathCommand::LineTo(Point::new(x + w, y + h)));
path.commands
.push(PathCommand::LineTo(Point::new(x, y + h)));
path.commands.push(PathCommand::Close);
self.current_point = Point::new(x, y);
}
b"h" => {
let path = self.path_mut();
path.commands.push(PathCommand::Close);
}
b"f" | b"F" => self.commit_path(true, false, FillRule::NonZero),
b"f*" => self.commit_path(true, false, FillRule::EvenOdd),
b"S" => self.commit_path(false, true, FillRule::NonZero),
b"s" => {
if let Some(p) = &mut self.current_path {
p.commands.push(PathCommand::Close);
}
self.commit_path(false, true, FillRule::NonZero);
}
b"B" => self.commit_path(true, true, FillRule::NonZero),
b"B*" => self.commit_path(true, true, FillRule::EvenOdd),
b"b" => {
if let Some(p) = &mut self.current_path {
p.commands.push(PathCommand::Close);
}
self.commit_path(true, true, FillRule::NonZero);
}
b"b*" => {
if let Some(p) = &mut self.current_path {
p.commands.push(PathCommand::Close);
}
self.commit_path(true, true, FillRule::EvenOdd);
}
b"n" => {
self.current_path = None;
self.operands.clear();
}
b"W" | b"W*" => {
if let Some(p) = self.current_path.take() {
self.current().clip = Some(p);
}
self.operands.clear();
}
b"rg" => {
let nums = self.take_numbers(3)?;
self.fill_cs = ColorSpaceKind::DeviceRgb;
self.fill_tiling = None;
self.fill_tiling_color = None;
self.fill_paint = Some(Paint::Solid(rgb_from_unit(nums[0], nums[1], nums[2])));
}
b"RG" => {
let nums = self.take_numbers(3)?;
self.stroke_cs = ColorSpaceKind::DeviceRgb;
self.stroke_tiling = None;
self.stroke_paint = Some(Paint::Solid(rgb_from_unit(nums[0], nums[1], nums[2])));
}
b"g" => {
let nums = self.take_numbers(1)?;
self.fill_cs = ColorSpaceKind::DeviceGray;
self.fill_tiling = None;
self.fill_tiling_color = None;
self.fill_paint = Some(Paint::Solid(rgb_from_unit(nums[0], nums[0], nums[0])));
}
b"G" => {
let nums = self.take_numbers(1)?;
self.stroke_cs = ColorSpaceKind::DeviceGray;
self.stroke_tiling = None;
self.stroke_paint = Some(Paint::Solid(rgb_from_unit(nums[0], nums[0], nums[0])));
}
b"k" | b"K" => {
let nums = self.take_numbers(4)?;
let p = Some(Paint::Solid(rgb_from_cmyk(
nums[0], nums[1], nums[2], nums[3],
)));
if op == b"K" {
self.stroke_cs = ColorSpaceKind::DeviceCmyk;
self.stroke_tiling = None;
self.stroke_paint = p;
} else {
self.fill_cs = ColorSpaceKind::DeviceCmyk;
self.fill_tiling = None;
self.fill_tiling_color = None;
self.fill_paint = p;
}
}
b"sc" | b"scn" => {
self.fill_tiling = self.tiling_pattern_name_from_operand();
self.fill_tiling_color = self
.fill_tiling
.as_ref()
.and_then(|n| self.uncoloured_tiling_color(n));
let paint = self
.color_from_components(&self.fill_cs.clone())
.or_else(|| self.pattern_paint_from_operand());
self.fill_paint = paint.or_else(|| {
self.fill_paint
.clone()
.or(Some(Paint::Solid(Rgba::opaque(0, 0, 0))))
});
self.operands.clear();
}
b"SC" | b"SCN" => {
self.stroke_tiling = self.tiling_pattern_name_from_operand();
let paint = self
.color_from_components(&self.stroke_cs.clone())
.or_else(|| self.pattern_paint_from_operand());
self.stroke_paint = paint.or_else(|| {
self.stroke_paint
.clone()
.or(Some(Paint::Solid(Rgba::opaque(0, 0, 0))))
});
self.operands.clear();
}
b"cs" => {
self.fill_cs = self.take_color_space_name();
self.fill_tiling = None;
self.fill_tiling_color = None;
self.fill_paint = initial_color_for(&self.fill_cs);
self.operands.clear();
}
b"CS" => {
self.stroke_cs = self.take_color_space_name();
self.stroke_tiling = None;
self.stroke_paint = initial_color_for(&self.stroke_cs);
self.operands.clear();
}
b"w" => {
let nums = self.take_numbers(1)?;
self.stroke_width = nums[0];
}
b"J" => {
let nums = self.take_numbers(1)?;
self.line_cap = match nums[0] as i32 {
0 => LineCap::Butt,
1 => LineCap::Round,
2 => LineCap::Square,
_ => LineCap::Butt,
};
}
b"j" => {
let nums = self.take_numbers(1)?;
self.line_join = match nums[0] as i32 {
0 => LineJoin::Miter,
1 => LineJoin::Round,
2 => LineJoin::Bevel,
_ => LineJoin::Miter,
};
}
b"M" => {
let nums = self.take_numbers(1)?;
self.miter_limit = nums[0];
}
b"d" => {
if self.operands.len() < 2 {
self.operands.clear();
return Ok(());
}
let offset = match self.operands.pop().unwrap() {
Operand::Number(n) => n,
_ => 0.0,
};
let array = match self.operands.pop().unwrap() {
Operand::Array(v) => v
.into_iter()
.filter_map(|el| match el {
ArrayElem::Number(n) => Some(n),
ArrayElem::String(_) => None,
})
.collect::<Vec<f32>>(),
_ => Vec::new(),
};
self.dash = if array.is_empty() {
None
} else {
Some(DashPattern { array, offset })
};
self.operands.clear();
}
b"BT" => {
self.text_matrix = Transform2D::identity();
self.text_line_matrix = Transform2D::identity();
self.in_text_object = true;
self.operands.clear();
}
b"ET" => {
self.in_text_object = false;
self.operands.clear();
}
b"Tf" => {
let size = match self.operands.last() {
Some(Operand::Number(n)) => *n,
_ => 0.0,
};
let name = match self.operands.iter().rev().nth(1) {
Some(Operand::Name(s)) => s.clone(),
_ => String::new(),
};
self.current_font = Some((name, size));
self.operands.clear();
}
b"TL" => {
if let Some(Operand::Number(n)) = self.operands.last() {
self.text_leading = *n;
}
self.operands.clear();
}
b"Tc" => {
if let Some(Operand::Number(n)) = self.operands.last() {
self.char_spacing = *n;
}
self.operands.clear();
}
b"Tw" => {
if let Some(Operand::Number(n)) = self.operands.last() {
self.word_spacing = *n;
}
self.operands.clear();
}
b"Tz" => {
if let Some(Operand::Number(n)) = self.operands.last() {
self.horiz_scale = *n / 100.0;
}
self.operands.clear();
}
b"Tr" => {
if let Some(Operand::Number(n)) = self.operands.last() {
self.text_render_mode = *n as i64;
}
self.operands.clear();
}
b"Ts" => {
if let Some(Operand::Number(n)) = self.operands.last() {
self.text_rise = *n;
}
self.operands.clear();
}
b"Td" => {
if let Ok(nums) = self.take_numbers(2) {
let (tx, ty) = (nums[0], nums[1]);
let m = Transform2D {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: tx,
f: ty,
};
self.text_line_matrix = compose(self.text_line_matrix, m);
self.text_matrix = self.text_line_matrix;
}
self.operands.clear();
}
b"TD" => {
if let Ok(nums) = self.take_numbers(2) {
let (tx, ty) = (nums[0], nums[1]);
self.text_leading = -ty;
let m = Transform2D {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: tx,
f: ty,
};
self.text_line_matrix = compose(self.text_line_matrix, m);
self.text_matrix = self.text_line_matrix;
}
self.operands.clear();
}
b"Tm" => {
if let Ok(nums) = self.take_numbers(6) {
let m = Transform2D {
a: nums[0],
b: nums[1],
c: nums[2],
d: nums[3],
e: nums[4],
f: nums[5],
};
self.text_matrix = m;
self.text_line_matrix = m;
}
self.operands.clear();
}
b"T*" => {
let leading = self.text_leading;
let m = Transform2D {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: -leading,
};
self.text_line_matrix = compose(self.text_line_matrix, m);
self.text_matrix = self.text_line_matrix;
self.operands.clear();
}
b"Tj" => {
let bytes = match self.operands.last() {
Some(Operand::String(s)) => s.clone(),
_ => Vec::new(),
};
let metrics = self.current_font_metrics();
self.emit_text_show(bytes.clone(), TextShowOp::Tj);
self.paint_type3_show(&bytes);
self.advance_text(&bytes, &metrics);
self.operands.clear();
}
b"TJ" => {
let metrics = self.current_font_metrics();
let mut bytes = Vec::new();
let mut elements: Vec<TjElem> = Vec::new();
if let Some(Operand::Array(items)) = self.operands.last() {
for el in items {
match el {
ArrayElem::String(s) => {
bytes.extend_from_slice(s);
elements.push(TjElem::Str(s.clone()));
}
ArrayElem::Number(n) => elements.push(TjElem::Kern(*n)),
}
}
}
self.emit_text_show(bytes, TextShowOp::TJ);
let tfs = self.current_font.as_ref().map(|(_, s)| *s).unwrap_or(0.0);
let th = self.horiz_scale;
for el in elements {
match el {
TjElem::Str(s) => {
self.paint_type3_show(&s);
self.advance_text(&s, &metrics);
}
TjElem::Kern(adj) => {
let tx = -adj / 1000.0 * tfs * th;
self.translate_text(tx);
}
}
}
self.operands.clear();
}
b"'" => {
let leading = self.text_leading;
let m = Transform2D {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: -leading,
};
self.text_line_matrix = compose(self.text_line_matrix, m);
self.text_matrix = self.text_line_matrix;
let bytes = match self.operands.last() {
Some(Operand::String(s)) => s.clone(),
_ => Vec::new(),
};
let metrics = self.current_font_metrics();
self.emit_text_show(bytes.clone(), TextShowOp::SingleQuote);
self.paint_type3_show(&bytes);
self.advance_text(&bytes, &metrics);
self.operands.clear();
}
b"\"" => {
let (aw, ac) = match (
self.operands.iter().rev().nth(2),
self.operands.iter().rev().nth(1),
) {
(Some(Operand::Number(aw)), Some(Operand::Number(ac))) => (*aw, *ac),
_ => (self.word_spacing, self.char_spacing),
};
self.word_spacing = aw;
self.char_spacing = ac;
let leading = self.text_leading;
let m = Transform2D {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: 0.0,
f: -leading,
};
self.text_line_matrix = compose(self.text_line_matrix, m);
self.text_matrix = self.text_line_matrix;
let bytes = match self.operands.last() {
Some(Operand::String(s)) => s.clone(),
_ => Vec::new(),
};
let metrics = self.current_font_metrics();
self.emit_text_show(bytes.clone(), TextShowOp::DoubleQuote);
self.paint_type3_show(&bytes);
self.advance_text(&bytes, &metrics);
self.operands.clear();
}
b"d0" | b"d1" => {
self.operands.clear();
}
b"Do" => {
let name = match self.operands.last() {
Some(Operand::Name(n)) => n.clone(),
_ => String::new(),
};
if !name.is_empty() {
if let Some(form) = self.xobject_forms.and_then(|m| m.get(&name)) {
if !form.children.is_empty() {
self.current().children.push(Node::Group(form.clone()));
}
}
}
self.operands.clear();
}
b"sh" => {
let name = match self.operands.last() {
Some(Operand::Name(n)) => n.clone(),
_ => String::new(),
};
let shading_dict = match (self.shading_resources, name.as_str()) {
(Some(res), n) if !n.is_empty() => lookup_dict(res, n).cloned(),
_ => None,
};
let mesh = shading_dict
.as_ref()
.and_then(|d| evaluate_mesh_shading(d, self.color_space_resources));
let gradient = shading_dict
.as_ref()
.and_then(|d| evaluate_gradient_shading(d, self.color_space_resources));
let ctm = self.effective_ctm();
let clip = self.current_clip();
if let Some(clip_path) = &clip {
if let Some(paint) = gradient
.as_ref()
.and_then(|g| gradient_to_paint(g, Transform2D::identity()))
{
let node = Node::Path(PathNode {
path: clip_path.clone(),
fill: Some(apply_alpha(paint, self.fill_alpha)),
stroke: None,
fill_rule: FillRule::NonZero,
});
self.current().children.push(node);
}
}
self.shadings.push(ContentShading {
name,
shading_dict,
ctm,
clip,
mesh,
gradient,
});
self.operands.clear();
}
b"MP" => {
let tag = self.last_name_operand();
let depth = self.mc_depth;
self.marked_content.push(ContentMarkedContent {
operator: MarkedContentOp::Mp,
tag,
properties: None,
depth,
});
self.operands.clear();
}
b"DP" => {
let (tag, properties) = self.marked_content_tag_props();
let depth = self.mc_depth;
self.marked_content.push(ContentMarkedContent {
operator: MarkedContentOp::Dp,
tag,
properties,
depth,
});
self.operands.clear();
}
b"BMC" => {
let tag = self.last_name_operand();
let depth = self.mc_depth;
self.marked_content.push(ContentMarkedContent {
operator: MarkedContentOp::Bmc,
tag,
properties: None,
depth,
});
self.mc_depth = self.mc_depth.saturating_add(1);
self.operands.clear();
}
b"BDC" => {
let (tag, properties) = self.marked_content_tag_props();
let depth = self.mc_depth;
self.marked_content.push(ContentMarkedContent {
operator: MarkedContentOp::Bdc,
tag,
properties,
depth,
});
self.mc_depth = self.mc_depth.saturating_add(1);
self.operands.clear();
}
b"EMC" => {
self.mc_depth = self.mc_depth.saturating_sub(1);
let depth = self.mc_depth;
self.marked_content.push(ContentMarkedContent {
operator: MarkedContentOp::Emc,
tag: String::new(),
properties: None,
depth,
});
self.operands.clear();
}
_ => {
self.operands.clear();
}
}
Ok(())
}
fn last_name_operand(&self) -> String {
self.operands
.iter()
.rev()
.find_map(|o| match o {
Operand::Name(n) => Some(n.clone()),
_ => None,
})
.unwrap_or_default()
}
fn marked_content_tag_props(&self) -> (String, Option<Dict>) {
let tag = self
.operands
.iter()
.find_map(|o| match o {
Operand::Name(n) => Some(n.clone()),
_ => None,
})
.unwrap_or_default();
let properties = match self.operands.last() {
Some(Operand::Dict(d)) => Some(d.clone()),
Some(Operand::Name(n)) if self.operands.len() >= 2 => self
.properties_resources
.and_then(|res| lookup_dict(res, n).cloned()),
_ => None,
};
(tag, properties)
}
fn effective_ctm(&self) -> Transform2D {
let mut acc = Transform2D::identity();
for frame in &self.stack {
acc = compose(acc, frame.transform);
}
acc
}
fn current_clip(&self) -> Option<Path> {
self.stack.last().and_then(|f| f.clip.clone())
}
fn commit_path(&mut self, fill: bool, stroke: bool, rule: FillRule) {
let Some(path) = self.current_path.take() else {
self.operands.clear();
return;
};
let tiled = if fill {
self.emit_tiling_fill(&path, rule)
} else {
false
};
let fill_paint = if fill && !tiled {
let base = self
.fill_paint
.clone()
.unwrap_or(Paint::Solid(Rgba::opaque(0, 0, 0)));
Some(apply_alpha(base, self.fill_alpha))
} else {
None
};
let stroke_obj = if stroke {
let stroke_paint = self
.stroke_paint
.clone()
.unwrap_or(Paint::Solid(Rgba::opaque(0, 0, 0)));
Some(Stroke {
width: self.stroke_width,
paint: apply_alpha(stroke_paint, self.stroke_alpha),
cap: self.line_cap,
join: self.line_join,
miter_limit: self.miter_limit,
dash: self.dash.clone(),
})
} else {
None
};
let node = Node::Path(PathNode {
path,
fill: fill_paint,
stroke: stroke_obj,
fill_rule: rule,
});
self.current().children.push(node);
self.operands.clear();
}
fn emit_tiling_fill(&mut self, fill_path: &Path, rule: FillRule) -> bool {
let Some(name) = self.fill_tiling.clone() else {
return false;
};
let Some(pat) = self.tiling_patterns.and_then(|m| m.get(&name)) else {
return false;
};
if pat.cell.children.is_empty() {
return false;
}
let xstep = pat.xstep;
let ystep = pat.ystep;
if !xstep.is_finite() || !ystep.is_finite() || xstep == 0.0 || ystep == 0.0 {
return false;
}
let mut above_root = Transform2D::identity();
for frame in self.stack.iter().skip(1) {
above_root = compose(above_root, frame.transform);
}
let region_path = transform_path(fill_path, above_root);
let Some((rx0, ry0, rx1, ry1)) = path_bounds(®ion_path) else {
return false;
};
let Some(inv) = invert_transform(pat.matrix) else {
return false;
};
let corners = [
inv.apply(Point::new(rx0, ry0)),
inv.apply(Point::new(rx1, ry0)),
inv.apply(Point::new(rx0, ry1)),
inv.apply(Point::new(rx1, ry1)),
];
let (mut px0, mut py0, mut px1, mut py1) = (
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
);
for c in corners {
if !c.x.is_finite() || !c.y.is_finite() {
return false;
}
px0 = px0.min(c.x);
py0 = py0.min(c.y);
px1 = px1.max(c.x);
py1 = py1.max(c.y);
}
let i_lo = (px0 / xstep).floor() as i64 - 1;
let i_hi = (px1 / xstep).ceil() as i64 + 1;
let j_lo = (py0 / ystep).floor() as i64 - 1;
let j_hi = (py1 / ystep).ceil() as i64 + 1;
let (i_lo, i_hi) = (i_lo.min(i_hi), i_lo.max(i_hi));
let (j_lo, j_hi) = (j_lo.min(j_hi), j_lo.max(j_hi));
let tile_count = (i_hi - i_lo + 1).saturating_mul(j_hi - j_lo + 1);
if tile_count <= 0 || tile_count > MAX_TILING_CELLS {
return false;
}
let bbox_clip = rect_path(pat.bbox[0], pat.bbox[1], pat.bbox[2], pat.bbox[3]);
let stencil_color = if pat.paint_type == 2 {
Some(self.fill_tiling_color.unwrap_or(Rgba::opaque(0, 0, 0)))
} else {
None
};
let mut tiles: Vec<Node> = Vec::new();
for j in j_lo..=j_hi {
for i in i_lo..=i_hi {
let placement = compose(
pat.matrix,
Transform2D::translate(i as f32 * xstep, j as f32 * ystep),
);
let mut cell = pat.cell.clone();
cell.transform = placement;
cell.clip = Some(bbox_clip.clone());
if let Some(color) = stencil_color {
for child in &mut cell.children {
recolor_node(child, color);
}
}
tiles.push(Node::Group(cell));
}
}
if tiles.is_empty() {
return false;
}
let mut region_clip = region_path;
let _ = rule;
if region_clip.commands.is_empty() {
return false;
}
let group = Group {
transform: Transform2D::identity(),
opacity: 1.0,
clip: Some(std::mem::take(&mut region_clip)),
children: tiles,
..Group::default()
};
self.stack[0].children.push(Node::Group(group));
true
}
fn emit_text_show(&mut self, bytes: Vec<u8>, operator: TextShowOp) {
if !self.in_text_object || self.font_resources.is_none() {
return;
}
let (font_name, font_size) = match &self.current_font {
Some((n, s)) => (n.clone(), *s),
None => (String::new(), 0.0),
};
let font_dict = match self.font_resources {
Some(fr) if !font_name.is_empty() => lookup_dict(fr, &font_name).cloned(),
_ => None,
};
self.text_shows.push(ContentTextShow {
font_name,
font_size,
font_dict,
bytes,
position: (self.text_matrix.e, self.text_matrix.f),
operator,
});
}
fn current_font_metrics(&self) -> FontMetrics {
let name = match &self.current_font {
Some((n, _)) if !n.is_empty() => n.as_str(),
_ => return FontMetrics::None,
};
match self.font_resources.and_then(|fr| lookup_dict(fr, name)) {
Some(d) => build_font_metrics(d),
None => FontMetrics::None,
}
}
fn advance_text(&mut self, bytes: &[u8], metrics: &FontMetrics) {
let tfs = self.current_font.as_ref().map(|(_, s)| *s).unwrap_or(0.0);
let th = self.horiz_scale;
let tc = self.char_spacing;
let scale = metrics.text_scale();
if metrics.two_byte() {
let mut i = 0;
while i + 1 < bytes.len() {
let cid = ((bytes[i] as i64) << 8) | bytes[i + 1] as i64;
let w0 = metrics.width(cid) * scale;
let tx = (w0 * tfs + tc) * th;
self.translate_text(tx);
i += 2;
}
} else {
for &b in bytes {
let w0 = metrics.width(b as i64) * scale;
let tw = if b == 32 { self.word_spacing } else { 0.0 };
let tx = (w0 * tfs + tc + tw) * th;
self.translate_text(tx);
}
}
}
fn paint_type3_show(&mut self, bytes: &[u8]) {
if self.text_render_mode == 3 {
return;
}
if self.type3_depth >= MAX_TYPE3_DEPTH {
return;
}
let font_name = match &self.current_font {
Some((n, _)) if !n.is_empty() => n.clone(),
_ => return,
};
let font = match self.type3_fonts.and_then(|m| m.get(&font_name)) {
Some(f) => f,
None => return,
};
let tfs = self.current_font.as_ref().map(|(_, s)| *s).unwrap_or(0.0);
let th = self.horiz_scale;
let tc = self.char_spacing;
let word_spacing = self.word_spacing;
let rise = self.text_rise;
let text_state = Transform2D {
a: tfs * th,
b: 0.0,
c: 0.0,
d: tfs,
e: 0.0,
f: rise,
};
let metrics = self.current_font_metrics();
let scale = metrics.text_scale();
let fill_color = match &self.fill_paint {
Some(Paint::Solid(c)) => *c,
_ => Rgba::opaque(0, 0, 0),
};
let mut tm = self.text_matrix;
let mut nodes: Vec<Node> = Vec::new();
for &b in bytes {
if let Some((glyph_name, glyph)) = font
.encoding
.get(&b)
.and_then(|n| font.glyphs.get(n).map(|g| (n, g)))
{
if !glyph.children.is_empty() {
let outer = compose(tm, text_state);
let g_xform = compose(outer, font.font_matrix);
let mut children = glyph.children.clone();
if font.shape_only.contains(glyph_name) {
for child in &mut children {
recolor_node(child, fill_color);
}
}
nodes.push(Node::Group(Group {
transform: g_xform,
children,
..Group::default()
}));
}
}
let w0 = metrics.width(b as i64) * scale;
let tw = if b == 32 { word_spacing } else { 0.0 };
let tx = (w0 * tfs + tc + tw) * th;
tm = compose(
tm,
Transform2D {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: tx,
f: 0.0,
},
);
}
if nodes.is_empty() {
return;
}
self.type3_depth += 1;
self.current().children.extend(nodes);
self.type3_depth -= 1;
}
fn translate_text(&mut self, tx: f32) {
let m = Transform2D {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
e: tx,
f: 0.0,
};
self.text_matrix = compose(self.text_matrix, m);
}
fn apply_ext_gstate(&mut self, dict: &Dict) {
for (k, v) in dict.entries() {
match k.as_str() {
"LW" => {
if let Some(n) = number_as_f32(v) {
self.stroke_width = n;
}
}
"LC" => {
if let Some(i) = number_as_i64(v) {
self.line_cap = match i {
0 => LineCap::Butt,
1 => LineCap::Round,
2 => LineCap::Square,
_ => self.line_cap,
};
}
}
"LJ" => {
if let Some(i) = number_as_i64(v) {
self.line_join = match i {
0 => LineJoin::Miter,
1 => LineJoin::Round,
2 => LineJoin::Bevel,
_ => self.line_join,
};
}
}
"ML" => {
if let Some(n) = number_as_f32(v) {
self.miter_limit = n;
}
}
"D" => {
if let Some((array, offset)) = parse_dash_pair(v) {
self.dash = if array.is_empty() {
None
} else {
Some(DashPattern { array, offset })
};
}
}
"CA" => {
if let Some(n) = number_as_f32(v) {
self.stroke_alpha = n.clamp(0.0, 1.0);
}
}
"ca" => {
if let Some(n) = number_as_f32(v) {
self.fill_alpha = n.clamp(0.0, 1.0);
}
}
_ => {}
}
}
}
fn path_mut(&mut self) -> &mut Path {
if self.current_path.is_none() {
self.current_path = Some(Path::new());
}
self.current_path.as_mut().unwrap()
}
fn take_numbers(&mut self, n: usize) -> Result<Vec<f32>, PdfError> {
if self.operands.len() < n {
return Err(PdfError::other(format!(
"PDF content parser: operator needed {n} numeric operands, got {}",
self.operands.len()
)));
}
let split = self.operands.len() - n;
let tail: Vec<Operand> = self.operands.drain(split..).collect();
let mut out = Vec::with_capacity(n);
for op in tail {
match op {
Operand::Number(f) => out.push(f),
other => {
return Err(PdfError::other(format!(
"PDF content parser: expected numeric operand, got {other:?}"
)));
}
}
}
Ok(out)
}
fn take_point(&mut self) -> Result<Point, PdfError> {
let nums = self.take_numbers(2)?;
Ok(Point::new(nums[0], nums[1]))
}
fn color_from_components(&self, cs: &ColorSpaceKind) -> Option<Paint> {
let want = cs.components()?;
if matches!(self.operands.last(), Some(Operand::Name(_))) {
return None;
}
let nums: Vec<f32> = self
.operands
.iter()
.rev()
.take_while(|o| matches!(o, Operand::Number(_)))
.filter_map(|o| match o {
Operand::Number(n) => Some(*n),
_ => None,
})
.collect();
if nums.len() < want {
return None;
}
let comps: Vec<f32> = nums.iter().take(want).rev().copied().collect();
Some(match cs {
ColorSpaceKind::DeviceGray => Paint::Solid(rgb_from_unit(comps[0], comps[0], comps[0])),
ColorSpaceKind::DeviceRgb => Paint::Solid(rgb_from_unit(comps[0], comps[1], comps[2])),
ColorSpaceKind::DeviceCmyk => {
Paint::Solid(rgb_from_cmyk(comps[0], comps[1], comps[2], comps[3]))
}
ColorSpaceKind::Indexed { base, hival, table } => {
return indexed_color(base, *hival, table, comps[0])
}
ColorSpaceKind::Separation {
alt,
tint,
none_colorant,
} => return separation_color(alt, tint, *none_colorant, comps[0]),
ColorSpaceKind::DeviceN {
alt,
tint,
all_none,
..
} => return device_n_color(alt, tint, *all_none, &comps),
ColorSpaceKind::CalGray { .. }
| ColorSpaceKind::CalRgb { .. }
| ColorSpaceKind::Lab { .. } => return cie_color(cs, &comps),
ColorSpaceKind::Unknown => unreachable!("components() returned Some"),
})
}
fn take_color_space_name(&mut self) -> ColorSpaceKind {
match self.operands.last() {
Some(Operand::Name(n)) => {
ColorSpaceKind::resolve_with_resources(n, self.color_space_resources)
}
_ => ColorSpaceKind::Unknown,
}
}
fn pattern_paint_from_operand(&self) -> Option<Paint> {
let name = match self.operands.last() {
Some(Operand::Name(n)) => n.as_str(),
_ => return None,
};
let pat = lookup_dict(self.pattern_resources?, name)?;
let get = |k: &str| pat.entries().iter().find(|(kk, _)| kk == k).map(|(_, v)| v);
if get("PatternType").and_then(number_as_i64) != Some(2) {
return None;
}
let Some(Object::Dict(shading)) = get("Shading") else {
return None;
};
let gradient = evaluate_gradient_shading(shading, self.color_space_resources)?;
let pattern_matrix = match get("Matrix").and_then(read_num_array) {
Some(m) if m.len() == 6 => Transform2D {
a: m[0],
b: m[1],
c: m[2],
d: m[3],
e: m[4],
f: m[5],
},
Some(_) => return None,
None => Transform2D::identity(),
};
let to_target = compose(self.effective_ctm(), pattern_matrix);
gradient_to_paint(&gradient, to_target)
}
fn tiling_pattern_name_from_operand(&self) -> Option<String> {
let name = match self.operands.last() {
Some(Operand::Name(n)) => n.as_str(),
_ => return None,
};
if self.tiling_patterns?.contains_key(name) {
Some(name.to_string())
} else {
None
}
}
fn uncoloured_tiling_color(&self, name: &str) -> Option<Rgba> {
let pat = self.tiling_patterns?.get(name)?;
if pat.paint_type != 2 {
return None;
}
let comps: Vec<f32> = self
.operands
.iter()
.filter_map(|o| match o {
Operand::Number(n) => Some(*n),
_ => None,
})
.collect();
let paint = match comps.len() {
1 => Paint::Solid(rgb_from_unit(comps[0], comps[0], comps[0])),
3 => Paint::Solid(rgb_from_unit(comps[0], comps[1], comps[2])),
4 => Paint::Solid(rgb_from_cmyk(comps[0], comps[1], comps[2], comps[3])),
_ => return None,
};
match paint {
Paint::Solid(c) => Some(c),
_ => None,
}
}
fn parse(&mut self, input: &[u8]) -> Result<(), PdfError> {
let mut i = 0;
while i < input.len() {
let b = input[i];
if is_whitespace(b) {
i += 1;
continue;
}
if b == b'%' {
while i < input.len() && input[i] != b'\n' && input[i] != b'\r' {
i += 1;
}
continue;
}
if b == b'(' {
let (end, bytes) = read_literal_string(input, i)?;
self.operands.push(Operand::String(bytes));
i = end;
continue;
}
if b == b'<' && input.get(i + 1) == Some(&b'<') {
let mut p = crate::reader::parse::Parser::new(&input[i..]);
match p.parse_object() {
Ok(Some(Object::Dict(d))) => {
self.operands.push(Operand::Dict(d));
i += p.position();
continue;
}
_ => {
i += 2;
continue;
}
}
}
if b == b'<' && input.get(i + 1) != Some(&b'<') {
let (end, bytes) = read_hex_string(input, i)?;
self.operands.push(Operand::String(bytes));
i = end;
continue;
}
if b == b'[' {
let (end, items) = read_array(input, i)?;
self.operands.push(Operand::Array(items));
i = end;
continue;
}
if b == b'/' {
let mut end = i + 1;
while end < input.len() && !is_whitespace(input[end]) && !is_delimiter(input[end]) {
end += 1;
}
let name = String::from_utf8_lossy(&input[i + 1..end]).into_owned();
self.operands.push(Operand::Name(name));
i = end;
continue;
}
if matches!(b, b'+' | b'-' | b'.' | b'0'..=b'9') {
match scan_number(input, i) {
NumScan::Fast(end, f) => {
self.operands.push(Operand::Number(f));
i = end;
continue;
}
NumScan::Slow(end) => {
let s = str::from_utf8(&input[i..end]).map_err(|_| {
PdfError::other(format!(
"PDF content parser: non-UTF-8 number at byte {i}"
))
})?;
let f: f32 = s.parse().map_err(|_| {
PdfError::other(format!(
"PDF content parser: invalid number `{s}` at byte {i}"
))
})?;
self.operands.push(Operand::Number(f));
i = end;
continue;
}
NumScan::NotANumber => {
let kw_end = scan_keyword_end(input, i);
let kw = &input[i..kw_end];
self.dispatch(kw)?;
i = kw_end;
continue;
}
}
}
let kw_end = scan_keyword_end(input, i);
if kw_end == i {
i += 1;
continue;
}
let kw = &input[i..kw_end];
if kw == b"BI" {
i = self.consume_inline_image(input, kw_end);
continue;
}
self.dispatch(kw)?;
i = kw_end;
}
Ok(())
}
fn consume_inline_image(&mut self, input: &[u8], after_bi: usize) -> usize {
self.operands.clear();
match parse_one_inline_image(input, after_bi) {
Ok((image, resume)) => {
let ctm = self.effective_ctm();
let clip = self.current_clip();
self.inline_images
.push(ContentInlineImage { image, ctm, clip });
resume
}
Err(_) => match find_inline_image_ei(input, after_bi) {
Some(ei) => ei + 2,
None => input.len(),
},
}
}
}
impl Frame {
fn new() -> Self {
Self {
transform: Transform2D::identity(),
children: Vec::new(),
clip: None,
}
}
fn is_effectively_empty(&self) -> bool {
self.children.is_empty() && self.clip.is_none() && self.transform.is_identity()
}
}
fn is_whitespace(b: u8) -> bool {
matches!(b, 0x00 | b'\t' | b'\n' | 0x0C | b'\r' | b' ')
}
fn is_delimiter(b: u8) -> bool {
matches!(
b,
b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
)
}
fn rgb_from_unit(r: f32, g: f32, b: f32) -> Rgba {
Rgba::opaque(unit_to_byte(r), unit_to_byte(g), unit_to_byte(b))
}
fn initial_color_for(cs: &ColorSpaceKind) -> Option<Paint> {
match cs {
ColorSpaceKind::DeviceGray | ColorSpaceKind::DeviceRgb | ColorSpaceKind::DeviceCmyk => {
Some(Paint::Solid(Rgba::opaque(0, 0, 0)))
}
ColorSpaceKind::Indexed { base, hival, table } => indexed_color(base, *hival, table, 0.0),
ColorSpaceKind::Separation {
alt,
tint,
none_colorant,
} => separation_color(alt, tint, *none_colorant, 1.0),
ColorSpaceKind::DeviceN {
n_in,
alt,
tint,
all_none,
} => device_n_color(alt, tint, *all_none, &vec![1.0; *n_in]),
ColorSpaceKind::CalGray { .. } => cie_color(cs, &[0.0]),
ColorSpaceKind::CalRgb { .. } | ColorSpaceKind::Lab { .. } => {
cie_color(cs, &[0.0, 0.0, 0.0])
}
ColorSpaceKind::Unknown => None,
}
}
fn indexed_color(base: &ColorSpaceKind, hival: u32, table: &[u8], index: f32) -> Option<Paint> {
let m = base.components()?;
let idx = if index.is_finite() {
let r = index.round();
r.clamp(0.0, hival as f32) as u32
} else {
0
};
let start = (idx as usize).checked_mul(m)?;
let entry = table.get(start..start + m)?;
let unit = |i: usize| entry[i] as f32 / 255.0;
Some(match base {
ColorSpaceKind::DeviceGray => Paint::Solid(rgb_from_unit(unit(0), unit(0), unit(0))),
ColorSpaceKind::DeviceRgb => Paint::Solid(rgb_from_unit(unit(0), unit(1), unit(2))),
ColorSpaceKind::DeviceCmyk => {
Paint::Solid(rgb_from_cmyk(unit(0), unit(1), unit(2), unit(3)))
}
ColorSpaceKind::CalGray { .. } | ColorSpaceKind::CalRgb { .. } => {
cie_color(base, &(0..m).map(unit).collect::<Vec<_>>())?
}
ColorSpaceKind::Lab { range, .. } => {
let l = unit(0) * 100.0;
let a = range[0] + unit(1) * (range[1] - range[0]);
let b = range[2] + unit(2) * (range[3] - range[2]);
cie_color(base, &[l, a, b])?
}
ColorSpaceKind::Indexed { .. }
| ColorSpaceKind::Separation { .. }
| ColorSpaceKind::DeviceN { .. }
| ColorSpaceKind::Unknown => Paint::Solid(Rgba::opaque(0, 0, 0)),
})
}
fn separation_color(
alt: &ColorSpaceKind,
tint: &PdfFunction,
none_colorant: bool,
tint_value: f32,
) -> Option<Paint> {
if none_colorant {
return None;
}
let t = tint_value.clamp(0.0, 1.0);
let comps = tint.eval(t);
paint_from_alt_components(alt, &comps)
}
fn paint_from_alt_components(alt: &ColorSpaceKind, comps: &[f32]) -> Option<Paint> {
match alt {
ColorSpaceKind::DeviceGray | ColorSpaceKind::DeviceRgb | ColorSpaceKind::DeviceCmyk => {
paint_from_device_components(alt, comps)
}
ColorSpaceKind::CalGray { .. }
| ColorSpaceKind::CalRgb { .. }
| ColorSpaceKind::Lab { .. } => cie_color(alt, comps),
_ => None,
}
}
fn device_n_color(
alt: &ColorSpaceKind,
tint: &PdfFunction,
all_none: bool,
tints: &[f32],
) -> Option<Paint> {
if all_none {
return None;
}
let clamped: Vec<f32> = tints.iter().map(|t| t.clamp(0.0, 1.0)).collect();
let comps = tint.eval_n(&clamped);
paint_from_alt_components(alt, &comps)
}
fn cie_color(cs: &ColorSpaceKind, comps: &[f32]) -> Option<Paint> {
match cs {
ColorSpaceKind::CalGray { white, gamma } if comps.len() == 1 => {
Some(Paint::Solid(cal_gray_color(*white, *gamma, comps[0])))
}
ColorSpaceKind::CalRgb { gamma, matrix } if comps.len() == 3 => Some(Paint::Solid(
cal_rgb_color(*gamma, *matrix, [comps[0], comps[1], comps[2]]),
)),
ColorSpaceKind::Lab { white, range } if comps.len() == 3 => {
let a = comps[1].clamp(range[0], range[1]);
let b = comps[2].clamp(range[2], range[3]);
Some(Paint::Solid(lab_color(*white, [comps[0], a, b])))
}
_ => None,
}
}
fn paint_from_device_components(cs: &ColorSpaceKind, comps: &[f32]) -> Option<Paint> {
match cs {
ColorSpaceKind::DeviceGray if comps.len() == 1 => {
Some(Paint::Solid(rgb_from_unit(comps[0], comps[0], comps[0])))
}
ColorSpaceKind::DeviceRgb if comps.len() == 3 => {
Some(Paint::Solid(rgb_from_unit(comps[0], comps[1], comps[2])))
}
ColorSpaceKind::DeviceCmyk if comps.len() == 4 => Some(Paint::Solid(rgb_from_cmyk(
comps[0], comps[1], comps[2], comps[3],
))),
_ => None,
}
}
fn rgba_from_components(cs: &ColorSpaceKind, comps: &[f32]) -> Option<Rgba> {
let paint = match cs {
ColorSpaceKind::DeviceGray | ColorSpaceKind::DeviceRgb | ColorSpaceKind::DeviceCmyk => {
paint_from_device_components(cs, comps)?
}
ColorSpaceKind::Indexed { base, hival, table } => {
indexed_color(base, *hival, table, *comps.first()?)?
}
ColorSpaceKind::Separation {
alt,
tint,
none_colorant,
} => separation_color(alt, tint, *none_colorant, *comps.first()?)?,
ColorSpaceKind::DeviceN {
alt,
tint,
all_none,
..
} => device_n_color(alt, tint, *all_none, comps)?,
ColorSpaceKind::CalGray { .. }
| ColorSpaceKind::CalRgb { .. }
| ColorSpaceKind::Lab { .. } => cie_color(cs, comps)?,
ColorSpaceKind::Unknown => return None,
};
match paint {
Paint::Solid(rgba) => Some(rgba),
_ => None,
}
}
struct BitReader<'a> {
data: &'a [u8],
bit_pos: usize,
}
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, bit_pos: 0 }
}
fn read(&mut self, bits: u32) -> Option<u64> {
let mut code: u64 = 0;
for _ in 0..bits {
let byte = *self.data.get(self.bit_pos / 8)?;
let bit = (byte >> (7 - (self.bit_pos % 8) as u32)) & 1;
code = (code << 1) | (bit as u64);
self.bit_pos += 1;
}
Some(code)
}
fn align_byte(&mut self) {
if self.bit_pos % 8 != 0 {
self.bit_pos = self.bit_pos.div_ceil(8) * 8;
}
}
fn at_end(&self) -> bool {
self.bit_pos / 8 >= self.data.len()
}
}
fn decode_value(code: u64, bits: u32, dmin: f32, dmax: f32) -> f32 {
let max_code = if bits >= 32 {
u32::MAX as f32
} else {
((1u64 << bits) - 1) as f32
};
if max_code == 0.0 {
return dmin;
}
dmin + (code as f32) * (dmax - dmin) / max_code
}
#[cfg(test)]
pub(crate) fn evaluate_mesh_shading_for_test(dict: &Dict) -> Option<MeshShading> {
evaluate_mesh_shading(dict, None)
}
fn shading_color_space(obj: &Object, color_space_resources: Option<&Dict>) -> ColorSpaceKind {
if let Object::Name(n) = obj {
return ColorSpaceKind::resolve_with_resources(n, color_space_resources);
}
color_space_from_object(obj)
}
fn evaluate_mesh_shading(dict: &Dict, color_space_resources: Option<&Dict>) -> Option<MeshShading> {
let get = |key: &str| {
dict.entries()
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v)
};
let shading_type = get("ShadingType").and_then(number_as_i64)?;
if !(4..=7).contains(&shading_type) {
return None;
}
let cs = shading_color_space(get("ColorSpace")?, color_space_resources);
if cs == ColorSpaceKind::Unknown {
return None;
}
let func = parse_shading_function(get("Function"));
let n_color = if func.is_some() { 1 } else { cs.components()? };
let bits_coord = get("BitsPerCoordinate").and_then(number_as_i64)? as u32;
if !matches!(bits_coord, 1 | 2 | 4 | 8 | 12 | 16 | 24 | 32) {
return None;
}
let bits_comp = get("BitsPerComponent").and_then(number_as_i64)? as u32;
if !matches!(bits_comp, 1 | 2 | 4 | 8 | 12 | 16) {
return None;
}
let decode = get("Decode").and_then(read_num_array)?;
if decode.len() != 4 + 2 * n_color {
return None;
}
let raw = match get("__MeshData") {
Some(Object::HexString(bytes)) => bytes.as_slice(),
_ => return None,
};
let bits_flag = if shading_type == 5 {
0
} else {
let bf = get("BitsPerFlag").and_then(number_as_i64)? as u32;
if !matches!(bf, 2 | 4 | 8) {
return None;
}
bf
};
let evaluator = MeshEvaluator {
cs: &cs,
func: func.as_ref(),
n_color,
bits_coord,
bits_comp,
bits_flag,
decode: &decode,
};
match shading_type {
4 => evaluator.eval_type4(raw),
5 => {
let vpr = get("VerticesPerRow").and_then(number_as_i64)?;
if vpr < 2 {
return None;
}
evaluator.eval_type5(raw, vpr as usize)
}
6 => evaluator.eval_patch(raw, bits_flag, false),
7 => evaluator.eval_patch(raw, bits_flag, true),
_ => None,
}
}
const GRADIENT_STOPS: usize = 64;
const FUNCTION_GRID: usize = 16;
fn stops_to_gradient_stops(stops: &[Rgba]) -> Vec<GradientStop> {
let n = stops.len();
stops
.iter()
.enumerate()
.map(|(i, c)| GradientStop {
offset: if n <= 1 {
0.0
} else {
i as f32 / (n - 1) as f32
},
color: *c,
})
.collect()
}
fn extend_to_spread(_extend: [bool; 2]) -> SpreadMethod {
SpreadMethod::Pad
}
fn gradient_to_paint(g: &ShadingGradient, to_target: Transform2D) -> Option<Paint> {
let scale = {
let det = (to_target.a * to_target.d - to_target.b * to_target.c).abs();
det.sqrt()
};
match g {
ShadingGradient::Axial {
coords,
extend,
stops,
} => {
let start = to_target.apply(Point::new(coords[0], coords[1]));
let end = to_target.apply(Point::new(coords[2], coords[3]));
Some(Paint::LinearGradient(LinearGradient {
start,
end,
stops: stops_to_gradient_stops(stops),
spread: extend_to_spread(*extend),
}))
}
ShadingGradient::Radial {
coords,
extend,
stops,
} => {
let focal = to_target.apply(Point::new(coords[0], coords[1]));
let center = to_target.apply(Point::new(coords[3], coords[4]));
let radius = coords[5] * scale;
Some(Paint::RadialGradient(RadialGradient {
center,
radius,
focal: Some(focal),
stops: stops_to_gradient_stops(stops),
spread: extend_to_spread(*extend),
}))
}
ShadingGradient::FunctionBased { .. } => None,
}
}
fn evaluate_gradient_shading(
dict: &Dict,
color_space_resources: Option<&Dict>,
) -> Option<ShadingGradient> {
let get = |key: &str| {
dict.entries()
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v)
};
let shading_type = get("ShadingType").and_then(number_as_i64)?;
if !(1..=3).contains(&shading_type) {
return None;
}
let cs = shading_color_space(get("ColorSpace")?, color_space_resources);
if cs == ColorSpaceKind::Unknown {
return None;
}
let func = parse_shading_function(get("Function"))?;
let extend = match get("Extend") {
Some(Object::Array(items)) if items.len() == 2 => [
matches!(items[0], Object::Bool(true)),
matches!(items[1], Object::Bool(true)),
],
_ => [false, false],
};
match shading_type {
2 => {
let coords_v = get("Coords").and_then(read_num_array)?;
if coords_v.len() != 4 {
return None;
}
let coords = [coords_v[0], coords_v[1], coords_v[2], coords_v[3]];
let (t0, t1) = shading_domain(get("Domain"));
let stops = sample_stops(&cs, &func, t0, t1)?;
Some(ShadingGradient::Axial {
coords,
extend,
stops,
})
}
3 => {
let coords_v = get("Coords").and_then(read_num_array)?;
if coords_v.len() != 6 {
return None;
}
let coords = [
coords_v[0],
coords_v[1],
coords_v[2],
coords_v[3],
coords_v[4],
coords_v[5],
];
let (t0, t1) = shading_domain(get("Domain"));
let stops = sample_stops(&cs, &func, t0, t1)?;
Some(ShadingGradient::Radial {
coords,
extend,
stops,
})
}
1 => {
let domain = match get("Domain").and_then(read_num_array) {
Some(d) if d.len() == 4 => [d[0], d[1], d[2], d[3]],
Some(_) => return None,
None => [0.0, 1.0, 0.0, 1.0],
};
let matrix = match get("Matrix").and_then(read_num_array) {
Some(m) if m.len() == 6 => Transform2D {
a: m[0],
b: m[1],
c: m[2],
d: m[3],
e: m[4],
f: m[5],
},
Some(_) => return None,
None => Transform2D::identity(),
};
let nx = FUNCTION_GRID;
let ny = FUNCTION_GRID;
let mut samples = Vec::with_capacity(nx * ny);
for j in 0..ny {
let y = lerp_domain(domain[2], domain[3], j, ny);
for i in 0..nx {
let x = lerp_domain(domain[0], domain[1], i, nx);
let comps = func.eval_n(&[x, y]);
samples.push(rgba_from_components(&cs, &comps)?);
}
}
Some(ShadingGradient::FunctionBased {
domain,
matrix,
grid: (nx, ny),
samples,
})
}
_ => None,
}
}
fn shading_domain(obj: Option<&Object>) -> (f32, f32) {
match obj.and_then(read_num_array) {
Some(d) if d.len() == 2 => (d[0], d[1]),
_ => (0.0, 1.0),
}
}
fn lerp_domain(lo: f32, hi: f32, k: usize, n: usize) -> f32 {
if n <= 1 {
return lo;
}
lo + (hi - lo) * (k as f32) / ((n - 1) as f32)
}
fn sample_stops(
cs: &ColorSpaceKind,
func: &ShadingFunction,
t0: f32,
t1: f32,
) -> Option<Vec<Rgba>> {
let mut stops = Vec::with_capacity(GRADIENT_STOPS);
for k in 0..GRADIENT_STOPS {
let t = lerp_domain(t0, t1, k, GRADIENT_STOPS);
let comps = func.eval(t);
stops.push(rgba_from_components(cs, &comps)?);
}
Some(stops)
}
fn parse_shading_function(obj: Option<&Object>) -> Option<ShadingFunction> {
match obj? {
Object::Array(items) => {
let parts: Vec<PdfFunction> = items
.iter()
.map(PdfFunction::parse)
.collect::<Option<_>>()?;
if parts.is_empty() {
return None;
}
Some(ShadingFunction::Array(parts))
}
single => Some(ShadingFunction::Single(PdfFunction::parse(single)?)),
}
}
enum ShadingFunction {
Single(PdfFunction),
Array(Vec<PdfFunction>),
}
impl ShadingFunction {
fn eval(&self, t: f32) -> Vec<f32> {
self.eval_n(&[t])
}
fn eval_n(&self, inputs: &[f32]) -> Vec<f32> {
match self {
ShadingFunction::Single(f) => f.eval_n(inputs),
ShadingFunction::Array(fs) => fs
.iter()
.filter_map(|f| f.eval_n(inputs).first().copied())
.collect(),
}
}
}
struct MeshEvaluator<'a> {
cs: &'a ColorSpaceKind,
func: Option<&'a ShadingFunction>,
n_color: usize,
bits_coord: u32,
bits_comp: u32,
bits_flag: u32,
decode: &'a [f32],
}
impl MeshEvaluator<'_> {
fn read_point(&self, r: &mut BitReader) -> Option<Point> {
let xc = r.read(self.bits_coord)?;
let yc = r.read(self.bits_coord)?;
let x = decode_value(xc, self.bits_coord, self.decode[0], self.decode[1]);
let y = decode_value(yc, self.bits_coord, self.decode[2], self.decode[3]);
Some(Point::new(x, y))
}
fn read_color(&self, r: &mut BitReader) -> Option<Rgba> {
if let Some(func) = self.func {
let code = r.read(self.bits_comp)?;
let t = decode_value(code, self.bits_comp, self.decode[4], self.decode[5]);
let comps = func.eval(t);
rgba_from_components(self.cs, &comps)
} else {
let mut comps = Vec::with_capacity(self.n_color);
for i in 0..self.n_color {
let code = r.read(self.bits_comp)?;
let dmin = self.decode[4 + 2 * i];
let dmax = self.decode[5 + 2 * i];
comps.push(decode_value(code, self.bits_comp, dmin, dmax));
}
rgba_from_components(self.cs, &comps)
}
}
fn read_vertex(&self, r: &mut BitReader) -> Option<MeshVertex> {
let point = self.read_point(r)?;
let color = self.read_color(r)?;
r.align_byte();
Some(MeshVertex { point, color })
}
fn eval_type4(&self, raw: &[u8]) -> Option<MeshShading> {
let bits_flag = self.bits_flag;
let mut r = BitReader::new(raw);
let mut triangles: Vec<MeshTriangle> = Vec::new();
let mut prev: Option<[MeshVertex; 3]> = None;
loop {
if r.at_end() {
break;
}
let f = match r.read(bits_flag) {
Some(v) => v & 0b11,
None => break,
};
let v = self.read_vertex(&mut r)?;
match f {
0 => {
r.read(bits_flag)?;
let vb = self.read_vertex(&mut r)?;
r.read(bits_flag)?;
let vc = self.read_vertex(&mut r)?;
let tri = [v, vb, vc];
triangles.push(MeshTriangle { vertices: tri });
prev = Some(tri);
}
1 => {
let [_va, vb, vc] = prev?;
let tri = [vb, vc, v];
triangles.push(MeshTriangle { vertices: tri });
prev = Some(tri);
}
2 => {
let [va, _vb, vc] = prev?;
let tri = [va, vc, v];
triangles.push(MeshTriangle { vertices: tri });
prev = Some(tri);
}
_ => return None,
}
}
if triangles.is_empty() {
return None;
}
Some(MeshShading::Triangles(triangles))
}
fn eval_type5(&self, raw: &[u8], vpr: usize) -> Option<MeshShading> {
let mut r = BitReader::new(raw);
let mut rows: Vec<Vec<MeshVertex>> = Vec::new();
loop {
if r.at_end() {
break;
}
let mut row = Vec::with_capacity(vpr);
for _ in 0..vpr {
match self.read_vertex(&mut r) {
Some(v) => row.push(v),
None => break,
}
}
if row.len() != vpr {
break;
}
rows.push(row);
}
if rows.len() < 2 {
return None;
}
let mut triangles = Vec::new();
for i in 0..rows.len() - 1 {
for j in 0..vpr - 1 {
let a = rows[i][j];
let b = rows[i][j + 1];
let c = rows[i + 1][j];
let d = rows[i + 1][j + 1];
triangles.push(MeshTriangle {
vertices: [a, b, c],
});
triangles.push(MeshTriangle {
vertices: [b, c, d],
});
}
}
Some(MeshShading::Triangles(triangles))
}
fn eval_patch(&self, raw: &[u8], bits_flag: u32, tensor: bool) -> Option<MeshShading> {
let mut r = BitReader::new(raw);
let mut patches: Vec<MeshPatch> = Vec::new();
loop {
if r.at_end() {
break;
}
let f = match r.read(bits_flag) {
Some(v) => v & 0b11,
None => break,
};
let new_pts = if f == 0 {
if tensor {
16
} else {
12
}
} else if tensor {
12
} else {
8
};
let mut pts = Vec::with_capacity(new_pts);
for _ in 0..new_pts {
pts.push(self.read_point(&mut r)?);
}
let new_cols = if f == 0 { 4 } else { 2 };
let mut cols = Vec::with_capacity(new_cols);
for _ in 0..new_cols {
cols.push(self.read_color(&mut r)?);
}
r.align_byte();
let patch = build_patch(f, tensor, &pts, &cols, patches.last())?;
patches.push(patch);
}
if patches.is_empty() {
return None;
}
Some(MeshShading::Patches(patches))
}
}
fn build_patch(
f: u64,
tensor: bool,
new_pts: &[Point],
new_cols: &[Rgba],
prev: Option<&MeshPatch>,
) -> Option<MeshPatch> {
const TENSOR_ORDER: [(usize, usize); 16] = [
(0, 0),
(0, 1),
(0, 2),
(0, 3),
(1, 3),
(2, 3),
(3, 3),
(3, 2),
(3, 1),
(3, 0),
(2, 0),
(1, 0),
(1, 1),
(1, 2),
(2, 2),
(2, 1),
];
let mut p = [[Point::new(0.0, 0.0); 4]; 4];
let boundary_slots = 12usize;
if f == 0 {
let count = if tensor { 16 } else { boundary_slots };
if new_pts.len() != count {
return None;
}
for (k, &pt) in new_pts.iter().enumerate() {
let (c, rr) = TENSOR_ORDER[k];
p[c][rr] = pt;
}
} else {
let prev = prev?;
let shared: [(usize, usize); 4] = match f {
1 => [(0, 3), (1, 3), (2, 3), (3, 3)], 2 => [(3, 3), (3, 2), (3, 1), (3, 0)], 3 => [(3, 0), (2, 0), (1, 0), (0, 0)], _ => return None,
};
for (k, &(c, rr)) in shared.iter().enumerate() {
let (tc, trr) = TENSOR_ORDER[k];
p[tc][trr] = prev.control_points[c][rr];
}
let count = if tensor { 16 } else { boundary_slots };
if new_pts.len() != count - 4 {
return None;
}
for (k, &pt) in new_pts.iter().enumerate() {
let (c, rr) = TENSOR_ORDER[k + 4];
p[c][rr] = pt;
}
}
if !tensor {
p[1][1] = coons_internal(
p[0][0], p[0][1], p[1][0], p[0][3], p[3][0], p[3][1], p[1][3], p[3][3],
);
p[1][2] = coons_internal(
p[0][3], p[0][2], p[1][3], p[0][0], p[3][3], p[3][2], p[1][0], p[3][0],
);
p[2][1] = coons_internal(
p[3][0], p[3][1], p[2][0], p[3][3], p[0][0], p[0][1], p[2][3], p[0][3],
);
p[2][2] = coons_internal(
p[3][3], p[3][2], p[2][3], p[3][0], p[0][3], p[0][2], p[2][0], p[0][0],
);
}
let corner_colors: [Rgba; 4] = if f == 0 {
if new_cols.len() != 4 {
return None;
}
[new_cols[0], new_cols[1], new_cols[2], new_cols[3]]
} else {
let prev = prev?;
if new_cols.len() != 2 {
return None;
}
let (c1, c2) = match f {
1 => (prev.corner_colors[1], prev.corner_colors[2]), 2 => (prev.corner_colors[2], prev.corner_colors[3]), 3 => (prev.corner_colors[3], prev.corner_colors[0]), _ => return None,
};
[c1, c2, new_cols[0], new_cols[1]]
};
Some(MeshPatch {
control_points: p,
corner_colors,
})
}
#[allow(clippy::too_many_arguments)]
fn coons_internal(
a: Point,
b: Point,
c: Point,
d: Point,
e: Point,
f: Point,
g: Point,
h: Point,
) -> Point {
let comp = |a: f32, b: f32, c: f32, d: f32, e: f32, f: f32, g: f32, h: f32| -> f32 {
(-4.0 * a + 6.0 * (b + c) - 2.0 * (d + e) + 3.0 * (f + g) - h) / 9.0
};
Point::new(
comp(a.x, b.x, c.x, d.x, e.x, f.x, g.x, h.x),
comp(a.y, b.y, c.y, d.y, e.y, f.y, g.y, h.y),
)
}
fn rgb_from_cmyk(cyan: f32, magenta: f32, yellow: f32, black: f32) -> Rgba {
let c = cyan.clamp(0.0, 1.0);
let m = magenta.clamp(0.0, 1.0);
let y = yellow.clamp(0.0, 1.0);
let k = black.clamp(0.0, 1.0);
let red = 1.0 - (c + k).min(1.0);
let green = 1.0 - (m + k).min(1.0);
let blue = 1.0 - (y + k).min(1.0);
rgb_from_unit(red, green, blue)
}
fn unit_to_byte(f: f32) -> u8 {
(f.clamp(0.0, 1.0) * 255.0).round() as u8
}
fn srgb_encode(c: f32) -> f32 {
let c = c.clamp(0.0, 1.0);
if c <= 0.003_130_8 {
12.92 * c
} else {
1.055 * c.powf(1.0 / 2.4) - 0.055
}
}
fn rgb_from_xyz(x: f32, y: f32, z: f32) -> Rgba {
let r = 3.240_625_5 * x - 1.537_208 * y - 0.498_628_6 * z;
let g = -0.968_930_7 * x + 1.875_756_1 * y + 0.041_517_5 * z;
let b = 0.055_710_1 * x - 0.204_021_1 * y + 1.056_995_9 * z;
rgb_from_unit(srgb_encode(r), srgb_encode(g), srgb_encode(b))
}
fn cal_gray_color(white: [f32; 3], gamma: f32, a: f32) -> Rgba {
let a = a.clamp(0.0, 1.0);
let decoded = a.powf(gamma);
rgb_from_xyz(white[0] * decoded, white[1] * decoded, white[2] * decoded)
}
fn cal_rgb_color(gamma: [f32; 3], matrix: [f32; 9], abc: [f32; 3]) -> Rgba {
let da = abc[0].clamp(0.0, 1.0).powf(gamma[0]);
let db = abc[1].clamp(0.0, 1.0).powf(gamma[1]);
let dc = abc[2].clamp(0.0, 1.0).powf(gamma[2]);
let x = matrix[0] * da + matrix[3] * db + matrix[6] * dc;
let y = matrix[1] * da + matrix[4] * db + matrix[7] * dc;
let z = matrix[2] * da + matrix[5] * db + matrix[8] * dc;
rgb_from_xyz(x, y, z)
}
fn lab_g(x: f32) -> f32 {
if x >= 6.0 / 29.0 {
x * x * x
} else {
(108.0 / 841.0) * (x - 4.0 / 29.0)
}
}
fn lab_color(white: [f32; 3], lab: [f32; 3]) -> Rgba {
let l = lab[0].clamp(0.0, 100.0);
let m_base = (l + 16.0) / 116.0;
let l_in = m_base + lab[1] / 500.0;
let n_in = m_base - lab[2] / 200.0;
rgb_from_xyz(
white[0] * lab_g(l_in),
white[1] * lab_g(m_base),
white[2] * lab_g(n_in),
)
}
const MAX_TILING_CELLS: i64 = 4096;
const MAX_TYPE3_DEPTH: u32 = 8;
fn recolor_node(node: &mut Node, color: Rgba) {
match node {
Node::Path(p) => {
if p.fill.is_some() {
p.fill = Some(Paint::Solid(color));
}
if let Some(stroke) = &mut p.stroke {
stroke.paint = Paint::Solid(color);
}
}
Node::Group(g) => {
for child in &mut g.children {
recolor_node(child, color);
}
}
_ => {}
}
}
fn rect_path(x0: f32, y0: f32, x1: f32, y1: f32) -> Path {
let mut p = Path::new();
p.commands.push(PathCommand::MoveTo(Point::new(x0, y0)));
p.commands.push(PathCommand::LineTo(Point::new(x1, y0)));
p.commands.push(PathCommand::LineTo(Point::new(x1, y1)));
p.commands.push(PathCommand::LineTo(Point::new(x0, y1)));
p.commands.push(PathCommand::Close);
p
}
fn transform_path(path: &Path, m: Transform2D) -> Path {
let mut out = Path::new();
out.commands.reserve(path.commands.len());
for cmd in &path.commands {
let mapped = match *cmd {
PathCommand::MoveTo(p) => PathCommand::MoveTo(m.apply(p)),
PathCommand::LineTo(p) => PathCommand::LineTo(m.apply(p)),
PathCommand::QuadCurveTo { control, end } => PathCommand::QuadCurveTo {
control: m.apply(control),
end: m.apply(end),
},
PathCommand::CubicCurveTo { c1, c2, end } => PathCommand::CubicCurveTo {
c1: m.apply(c1),
c2: m.apply(c2),
end: m.apply(end),
},
PathCommand::ArcTo {
rx,
ry,
x_axis_rot,
large_arc,
sweep,
end,
} => PathCommand::ArcTo {
rx,
ry,
x_axis_rot,
large_arc,
sweep,
end: m.apply(end),
},
PathCommand::Close => PathCommand::Close,
_ => *cmd,
};
out.commands.push(mapped);
}
out
}
fn path_bounds(path: &Path) -> Option<(f32, f32, f32, f32)> {
let (mut x0, mut y0, mut x1, mut y1) = (
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
);
let mut acc = |p: Point| {
x0 = x0.min(p.x);
y0 = y0.min(p.y);
x1 = x1.max(p.x);
y1 = y1.max(p.y);
};
for cmd in &path.commands {
match *cmd {
PathCommand::MoveTo(p) | PathCommand::LineTo(p) => acc(p),
PathCommand::QuadCurveTo { control, end } => {
acc(control);
acc(end);
}
PathCommand::CubicCurveTo { c1, c2, end } => {
acc(c1);
acc(c2);
acc(end);
}
PathCommand::ArcTo { end, .. } => acc(end),
PathCommand::Close => {}
_ => {}
}
}
if x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite() && x0 <= x1 && y0 <= y1
{
Some((x0, y0, x1, y1))
} else {
None
}
}
fn invert_transform(m: Transform2D) -> Option<Transform2D> {
let det = m.a * m.d - m.b * m.c;
if !det.is_finite() || det.abs() < f32::EPSILON {
return None;
}
let inv_det = 1.0 / det;
let a = m.d * inv_det;
let b = -m.b * inv_det;
let c = -m.c * inv_det;
let d = m.a * inv_det;
let e = -(a * m.e + c * m.f);
let f = -(b * m.e + d * m.f);
let inv = Transform2D { a, b, c, d, e, f };
if [inv.a, inv.b, inv.c, inv.d, inv.e, inv.f]
.iter()
.all(|v| v.is_finite())
{
Some(inv)
} else {
None
}
}
fn compose(a: Transform2D, b: Transform2D) -> Transform2D {
Transform2D {
a: a.a * b.a + a.c * b.b,
b: a.b * b.a + a.d * b.b,
c: a.a * b.c + a.c * b.d,
d: a.b * b.c + a.d * b.d,
e: a.a * b.e + a.c * b.f + a.e,
f: a.b * b.e + a.d * b.f + a.f,
}
}
fn scan_keyword_end(input: &[u8], start: usize) -> usize {
let mut end = start;
while end < input.len() && !is_whitespace(input[end]) && !is_delimiter(input[end]) {
end += 1;
}
end
}
const POW10_F32: [f32; 11] = [1.0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10];
enum NumScan {
NotANumber,
Fast(usize, f32),
Slow(usize),
}
fn scan_number(input: &[u8], start: usize) -> NumScan {
let mut end = start;
let neg = input[end] == b'-';
if matches!(input[end], b'+' | b'-') {
end += 1;
}
let mut mant: u64 = 0;
let mut frac: usize = 0;
let mut saw_digit = false;
let mut saw_dot = false;
let mut wide = false;
while end < input.len() {
let c = input[end];
if c.is_ascii_digit() {
saw_digit = true;
if mant < (1 << 56) {
mant = mant * 10 + (c - b'0') as u64;
} else {
wide = true;
}
if saw_dot {
frac += 1;
}
end += 1;
} else if c == b'.' && !saw_dot {
saw_dot = true;
end += 1;
} else {
break;
}
}
if !saw_digit {
return NumScan::NotANumber;
}
if !wide && mant < (1 << 24) && frac <= 10 {
let q = mant as f32 / POW10_F32[frac];
NumScan::Fast(end, if neg { -q } else { q })
} else {
NumScan::Slow(end)
}
}
fn read_literal_string(input: &[u8], start: usize) -> Result<(usize, Vec<u8>), PdfError> {
let mut end = start + 1;
let mut depth = 1u32;
let mut decoded = Vec::new();
while end < input.len() {
let b = input[end];
if b == b'\\' {
end += 1;
if end >= input.len() {
break;
}
let esc = input[end];
match esc {
b'n' => {
decoded.push(b'\n');
end += 1;
}
b'r' => {
decoded.push(b'\r');
end += 1;
}
b't' => {
decoded.push(b'\t');
end += 1;
}
b'b' => {
decoded.push(0x08);
end += 1;
}
b'f' => {
decoded.push(0x0C);
end += 1;
}
b'(' | b')' | b'\\' => {
decoded.push(esc);
end += 1;
}
b'\n' => {
end += 1;
}
b'\r' => {
end += 1;
if end < input.len() && input[end] == b'\n' {
end += 1;
}
}
d if d.is_ascii_digit() => {
let mut val: u16 = 0;
let mut n = 0;
while n < 3 && end < input.len() {
let c = input[end];
if !(b'0'..=b'7').contains(&c) {
break;
}
val = val * 8 + (c - b'0') as u16;
end += 1;
n += 1;
}
decoded.push((val & 0xFF) as u8);
}
other => {
decoded.push(other);
end += 1;
}
}
continue;
}
if b == b'(' {
depth += 1;
}
if b == b')' {
depth -= 1;
if depth == 0 {
end += 1;
return Ok((end, decoded));
}
}
decoded.push(b);
end += 1;
}
Err(PdfError::other(
"PDF content parser: unterminated literal string",
))
}
fn read_hex_string(input: &[u8], start: usize) -> Result<(usize, Vec<u8>), PdfError> {
let mut end = start + 1;
let mut nibbles: Vec<u8> = Vec::new();
while end < input.len() {
let c = input[end];
if c == b'>' {
if nibbles.len() % 2 == 1 {
nibbles.push(0);
}
let mut out = Vec::with_capacity(nibbles.len() / 2);
for pair in nibbles.chunks(2) {
out.push((pair[0] << 4) | pair[1]);
}
return Ok((end + 1, out));
}
if let Some(v) = hex_nibble(c) {
nibbles.push(v);
}
end += 1;
}
Err(PdfError::other(
"PDF content parser: unterminated hex string",
))
}
fn hex_nibble(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(10 + c - b'a'),
b'A'..=b'F' => Some(10 + c - b'A'),
_ => None,
}
}
fn lookup_dict<'a>(dict: &'a Dict, key: &str) -> Option<&'a Dict> {
dict.entries()
.iter()
.find(|(k, _)| k == key)
.and_then(|(_, v)| match v {
Object::Dict(d) => Some(d),
_ => None,
})
}
#[derive(Clone, Debug)]
enum FontMetrics {
Simple {
first_char: i64,
widths: Vec<f32>,
missing_width: f32,
text_scale: f32,
},
Cid {
default_width: f32,
ranges: Vec<(i64, Vec<f32>)>,
two_byte: bool,
},
None,
}
impl FontMetrics {
fn width(&self, code: i64) -> f32 {
match self {
FontMetrics::Simple {
first_char,
widths,
missing_width,
..
} => {
let idx = code - first_char;
if idx >= 0 && (idx as usize) < widths.len() {
widths[idx as usize]
} else {
*missing_width
}
}
FontMetrics::Cid {
default_width,
ranges,
..
} => {
for (start, run) in ranges {
let off = code - start;
if off >= 0 && (off as usize) < run.len() {
return run[off as usize];
}
}
*default_width
}
FontMetrics::None => 0.0,
}
}
fn two_byte(&self) -> bool {
matches!(self, FontMetrics::Cid { two_byte: true, .. })
}
fn text_scale(&self) -> f32 {
match self {
FontMetrics::Simple { text_scale, .. } => *text_scale,
_ => 0.001,
}
}
}
fn build_font_metrics(font: &Dict) -> FontMetrics {
let subtype = font
.entries()
.iter()
.find_map(|(k, v)| match (k.as_str(), v) {
("Subtype", Object::Name(s)) => Some(s.as_str()),
_ => None,
});
if subtype == Some("Type0") {
return build_cid_metrics(font);
}
let first_char = font
.entries()
.iter()
.find(|(k, _)| k == "FirstChar")
.and_then(|(_, v)| number_as_i64(v))
.unwrap_or(0);
let widths = match font.entries().iter().find(|(k, _)| k == "Widths") {
Some((_, Object::Array(items))) => items
.iter()
.map(|o| number_as_f32(o).unwrap_or(0.0))
.collect(),
_ => Vec::new(),
};
if widths.is_empty() {
return FontMetrics::None;
}
let missing_width = font
.entries()
.iter()
.find(|(k, _)| k == "FontDescriptor")
.and_then(|(_, v)| match v {
Object::Dict(d) => d
.entries()
.iter()
.find(|(k, _)| k == "MissingWidth")
.and_then(|(_, v)| number_as_f32(v)),
_ => None,
})
.unwrap_or(0.0);
let text_scale = if subtype == Some("Type3") {
font.entries()
.iter()
.find(|(k, _)| k == "FontMatrix")
.and_then(|(_, v)| match v {
Object::Array(items) if items.len() == 6 => number_as_f32(&items[0]),
_ => None,
})
.filter(|s| s.is_finite())
.unwrap_or(0.001)
} else {
0.001
};
FontMetrics::Simple {
first_char,
widths,
missing_width,
text_scale,
}
}
fn build_cid_metrics(font: &Dict) -> FontMetrics {
let two_byte = true;
let descendant = font
.entries()
.iter()
.find(|(k, _)| k == "DescendantFonts")
.and_then(|(_, v)| match v {
Object::Dict(d) => Some(d.clone()),
Object::Array(items) => items.iter().find_map(|o| match o {
Object::Dict(d) => Some(d.clone()),
_ => None,
}),
_ => None,
});
let Some(cid_font) = descendant else {
return FontMetrics::Cid {
default_width: 1000.0,
ranges: Vec::new(),
two_byte,
};
};
let default_width = cid_font
.entries()
.iter()
.find(|(k, _)| k == "DW")
.and_then(|(_, v)| number_as_f32(v))
.unwrap_or(1000.0);
let ranges = match cid_font.entries().iter().find(|(k, _)| k == "W") {
Some((_, Object::Array(items))) => parse_cid_widths(items),
_ => Vec::new(),
};
FontMetrics::Cid {
default_width,
ranges,
two_byte,
}
}
fn parse_cid_widths(items: &[Object]) -> Vec<(i64, Vec<f32>)> {
let mut out = Vec::new();
let mut i = 0;
while i < items.len() {
let Some(c) = number_as_i64(&items[i]) else {
i += 1;
continue;
};
match items.get(i + 1) {
Some(Object::Array(ws)) => {
let run: Vec<f32> = ws.iter().map(|o| number_as_f32(o).unwrap_or(0.0)).collect();
out.push((c, run));
i += 2;
}
Some(obj) => {
let clast = number_as_i64(obj);
let w = items.get(i + 2).and_then(number_as_f32);
match (clast, w) {
(Some(clast), Some(w)) if clast >= c => {
let count = (clast - c + 1).min(1 << 20) as usize;
out.push((c, vec![w; count]));
i += 3;
}
_ => {
i += 1;
}
}
}
None => break,
}
}
out
}
fn apply_alpha(paint: Paint, alpha: f32) -> Paint {
if (alpha - 1.0).abs() < f32::EPSILON {
return paint;
}
match paint {
Paint::Solid(rgba) => {
let base = rgba.a as f32 / 255.0;
let combined = (base * alpha).clamp(0.0, 1.0);
Paint::Solid(Rgba::new(
rgba.r,
rgba.g,
rgba.b,
(combined * 255.0).round() as u8,
))
}
other => other,
}
}
fn number_as_f32(obj: &Object) -> Option<f32> {
match obj {
Object::Integer(i) => Some(*i as f32),
Object::Real(r) => Some(*r as f32),
_ => None,
}
}
fn number_as_i64(obj: &Object) -> Option<i64> {
match obj {
Object::Integer(i) => Some(*i),
Object::Real(r) => Some(*r as i64),
_ => None,
}
}
fn parse_dash_pair(obj: &Object) -> Option<(Vec<f32>, f32)> {
let Object::Array(items) = obj else {
return None;
};
if items.len() != 2 {
return None;
}
let Object::Array(arr_items) = &items[0] else {
return None;
};
let mut array = Vec::with_capacity(arr_items.len());
for it in arr_items {
array.push(number_as_f32(it)?);
}
let offset = number_as_f32(&items[1])?;
Some((array, offset))
}
fn read_array(input: &[u8], start: usize) -> Result<(usize, Vec<ArrayElem>), PdfError> {
let mut end = start + 1;
let mut items: Vec<ArrayElem> = Vec::new();
while end < input.len() && input[end] != b']' {
let b = input[end];
if is_whitespace(b) {
end += 1;
continue;
}
if b == b'(' {
let (next, bytes) = read_literal_string(input, end)?;
items.push(ArrayElem::String(bytes));
end = next;
continue;
}
if b == b'<' && input.get(end + 1) != Some(&b'<') {
let (next, bytes) = read_hex_string(input, end)?;
items.push(ArrayElem::String(bytes));
end = next;
continue;
}
if matches!(b, b'+' | b'-' | b'.' | b'0'..=b'9') {
let nstart = end;
match scan_number(input, nstart) {
NumScan::Fast(next, f) => {
items.push(ArrayElem::Number(f));
end = next;
}
NumScan::Slow(next) => {
if let Ok(s) = str::from_utf8(&input[nstart..next]) {
if let Ok(f) = s.parse::<f32>() {
items.push(ArrayElem::Number(f));
}
}
end = next;
}
NumScan::NotANumber => {
end = nstart + 1;
}
}
continue;
}
end += 1;
}
if end < input.len() {
end += 1;
} Ok((end, items))
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(input: &[u8]) -> Group {
parse_content_stream(input).unwrap()
}
#[test]
fn scan_number_matches_str_parse_bitwise() {
let mut cases: Vec<String> = vec![
"0",
"-0",
"+0",
"5",
"-5",
"+5",
"5.",
"-5.",
".5",
"-.5",
"+.5",
"0.5",
"595.0",
"842.75",
"0.0001",
"-0.0001",
"123456",
"-123456",
"16777215",
"16777216",
"16777217",
"-16777216",
"999999999",
"0.1234567890",
"0.12345678901",
"3.14159265358979",
"1000000.25",
"-1000000.25",
"0.000000001",
"99999999999999999999",
"-99999999999999999999.5",
"00042",
"-00042.50",
]
.into_iter()
.map(str::to_owned)
.collect();
let mut state = 0x1234_5678u32;
let mut xs = || {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
state
};
for _ in 0..2000 {
let int_len = (xs() % 13) as usize;
let frac_len = (xs() % 13) as usize;
if int_len == 0 && frac_len == 0 {
continue;
}
let mut s = String::new();
match xs() % 3 {
0 => s.push('-'),
1 => s.push('+'),
_ => {}
}
for _ in 0..int_len {
s.push(char::from(b'0' + (xs() % 10) as u8));
}
if frac_len > 0 {
s.push('.');
for _ in 0..frac_len {
s.push(char::from(b'0' + (xs() % 10) as u8));
}
}
cases.push(s);
}
for case in &cases {
let bytes = case.as_bytes();
let expected: f32 = case.parse().unwrap_or_else(|_| panic!("parse {case}"));
match scan_number(bytes, 0) {
NumScan::Fast(end, got) => {
assert_eq!(end, bytes.len(), "consumed all of `{case}`");
assert_eq!(
got.to_bits(),
expected.to_bits(),
"`{case}`: fast {got} vs parse {expected}"
);
}
NumScan::Slow(end) => {
assert_eq!(end, bytes.len(), "consumed all of `{case}`");
}
NumScan::NotANumber => panic!("`{case}` should scan as a number"),
}
}
}
#[test]
fn scan_number_rejects_bare_sign_and_dot() {
for case in [&b"-"[..], b"+", b".", b"-.", b"+.", b"-x", b".)"] {
assert!(
matches!(scan_number(case, 0), NumScan::NotANumber),
"{case:?} must not scan as a number"
);
}
}
#[test]
fn scan_number_stops_at_delimiters_and_whitespace() {
let input = b"-12.5]";
match scan_number(input, 0) {
NumScan::Fast(end, v) => {
assert_eq!(end, 5);
assert_eq!(v.to_bits(), (-12.5f32).to_bits());
}
_ => panic!("expected fast scan"),
}
let input = b"7 0 R";
match scan_number(input, 0) {
NumScan::Fast(end, v) => {
assert_eq!(end, 1);
assert_eq!(v, 7.0);
}
_ => panic!("expected fast scan"),
}
let input = b"1.2.3";
match scan_number(input, 0) {
NumScan::Fast(end, v) => {
assert_eq!(end, 3);
assert_eq!(v.to_bits(), (1.2f32).to_bits());
}
_ => panic!("expected fast scan"),
}
}
#[test]
fn empty_content_yields_empty_group() {
let g = parse(b"");
assert!(g.children.is_empty());
assert!(g.clip.is_none());
}
#[test]
fn rect_fill_round_trips() {
let bytes = b"q 1 0 0 rg 10 10 m 110 10 l 110 60 l 10 60 l h f Q\n";
let root = parse(bytes);
assert_eq!(root.children.len(), 1);
let Node::Group(g) = &root.children[0] else {
panic!("expected group")
};
assert_eq!(g.children.len(), 1);
let Node::Path(pn) = &g.children[0] else {
panic!("expected path")
};
assert_eq!(pn.path.commands.len(), 5);
assert!(matches!(pn.path.commands[0], PathCommand::MoveTo(p) if (p.x - 10.0).abs() < 1e-3));
assert!(matches!(pn.path.commands[4], PathCommand::Close));
assert_eq!(pn.fill_rule, FillRule::NonZero);
match &pn.fill {
Some(Paint::Solid(r)) => assert_eq!((r.r, r.g, r.b), (255, 0, 0)),
other => panic!("unexpected fill: {other:?}"),
}
assert!(pn.stroke.is_none());
}
#[test]
fn nested_q_groups_are_promoted_to_node_groups() {
let bytes = b"q q 1 0 0 1 5 5 cm 0 0 m 10 10 l S Q Q\n";
let root = parse(bytes);
assert_eq!(root.children.len(), 1);
let Node::Group(outer) = &root.children[0] else {
panic!()
};
assert_eq!(outer.children.len(), 1);
let Node::Group(inner) = &outer.children[0] else {
panic!()
};
assert!(!inner.transform.is_identity());
assert_eq!(inner.children.len(), 1);
}
#[test]
fn rectangle_operator_re_expands_to_subpath() {
let bytes = b"q 0.5 0.5 0.5 rg 10 20 30 40 re f Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
assert_eq!(p.path.commands.len(), 5);
assert!(
matches!(p.path.commands[0], PathCommand::MoveTo(pp) if pp.x == 10.0 && pp.y == 20.0)
);
assert!(
matches!(p.path.commands[1], PathCommand::LineTo(pp) if pp.x == 40.0 && pp.y == 20.0)
);
assert!(
matches!(p.path.commands[2], PathCommand::LineTo(pp) if pp.x == 40.0 && pp.y == 60.0)
);
assert!(
matches!(p.path.commands[3], PathCommand::LineTo(pp) if pp.x == 10.0 && pp.y == 60.0)
);
assert!(matches!(p.path.commands[4], PathCommand::Close));
}
#[test]
fn cubic_curve_roundtrips() {
let bytes = b"q 0 0 m 1 1 2 1 3 0 c S Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
assert!(matches!(
p.path.commands[1],
PathCommand::CubicCurveTo { c1, c2, end }
if c1.x == 1.0 && c1.y == 1.0 && c2.x == 2.0 && c2.y == 1.0 && end.x == 3.0 && end.y == 0.0
));
}
#[test]
fn fill_rule_evenodd_recognised() {
let bytes = b"q 0 0 m 10 0 l 10 10 l h f* Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
assert_eq!(p.fill_rule, FillRule::EvenOdd);
}
#[test]
fn cm_translate_lands_on_group_transform() {
let bytes = b"q 1 0 0 1 100 200 cm 0 0 m 5 5 l S Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
assert!((g.transform.e - 100.0).abs() < 1e-3);
assert!((g.transform.f - 200.0).abs() < 1e-3);
}
#[test]
fn stroke_style_w_j_m_d_recorded() {
let bytes = b"q 2.5 w 1 J 2 j 8 M [5 3] 1 d 0 0 0 RG 0 0 m 10 10 l S Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let s = p.stroke.as_ref().expect("stroke set");
assert!((s.width - 2.5).abs() < 1e-3);
assert!(matches!(s.cap, LineCap::Round));
assert!(matches!(s.join, LineJoin::Bevel));
assert!((s.miter_limit - 8.0).abs() < 1e-3);
let dash = s.dash.as_ref().expect("dash set");
assert_eq!(dash.array, vec![5.0, 3.0]);
assert!((dash.offset - 1.0).abs() < 1e-3);
}
#[test]
fn clip_w_assigns_to_group_clip() {
let bytes =
b"q 10 10 m 50 10 l 50 50 l 10 50 l h W n 0 0 0 rg 20 20 m 30 20 l 30 30 l h f Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!()
};
assert!(g.clip.is_some());
assert_eq!(g.children.len(), 1);
}
#[test]
fn cmyk_pure_inks_convert_per_10_3_5() {
assert_eq!(rgb_from_cmyk(1.0, 0.0, 0.0, 0.0), Rgba::opaque(0, 255, 255));
assert_eq!(rgb_from_cmyk(0.0, 1.0, 0.0, 0.0), Rgba::opaque(255, 0, 255));
assert_eq!(rgb_from_cmyk(0.0, 0.0, 1.0, 0.0), Rgba::opaque(255, 255, 0));
assert_eq!(rgb_from_cmyk(0.0, 0.0, 0.0, 1.0), Rgba::opaque(0, 0, 0));
assert_eq!(
rgb_from_cmyk(0.0, 0.0, 0.0, 0.0),
Rgba::opaque(255, 255, 255)
);
}
#[test]
fn cmyk_component_plus_black_clamps_at_one() {
let r = rgb_from_cmyk(0.7, 0.0, 0.0, 0.7);
assert_eq!(r.r, 0);
assert_eq!(r.g, (0.3f32 * 255.0).round() as u8);
assert_eq!(r.b, (0.3f32 * 255.0).round() as u8);
}
#[test]
fn cmyk_out_of_range_operands_clamp() {
assert_eq!(
rgb_from_cmyk(-0.5, 2.0, 0.0, 0.0),
rgb_from_cmyk(0.0, 1.0, 0.0, 0.0)
);
}
#[test]
fn k_and_upper_k_operators_apply_cmyk_conversion() {
let bytes = b"q 1 0 0 0 k 0 1 0 0 K 0 0 m 10 10 l 10 0 l h B Q\n";
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!("expected group")
};
let Node::Path(p) = &g.children[0] else {
panic!("expected path")
};
match &p.fill {
Some(Paint::Solid(c)) => assert_eq!((c.r, c.g, c.b), (0, 255, 255)),
other => panic!("unexpected fill: {other:?}"),
}
let s = p.stroke.as_ref().expect("stroke set");
match &s.paint {
Paint::Solid(c) => assert_eq!((c.r, c.g, c.b), (255, 0, 255)),
other => panic!("unexpected stroke paint: {other:?}"),
}
}
fn first_path(bytes: &[u8]) -> PathNode {
let root = parse(bytes);
let Node::Group(g) = &root.children[0] else {
panic!("expected group");
};
let Node::Path(p) = &g.children[0] else {
panic!("expected path");
};
p.clone()
}
fn fill_rgb(p: &PathNode) -> (u8, u8, u8) {
match &p.fill {
Some(Paint::Solid(c)) => (c.r, c.g, c.b),
other => panic!("unexpected fill: {other:?}"),
}
}
#[test]
fn cs_devicergb_then_sc_sets_rgb_fill() {
let bytes = b"q /DeviceRGB cs 1 0 0 sc 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(fill_rgb(&first_path(bytes)), (255, 0, 0));
}
#[test]
fn cs_devicegray_then_sc_sets_gray_fill() {
let bytes = b"q /DeviceGray cs 0.5 sc 0 0 m 10 10 l 10 0 l h f Q\n";
let (r, g, b) = fill_rgb(&first_path(bytes));
let expect = (0.5f32 * 255.0).round() as u8;
assert_eq!((r, g, b), (expect, expect, expect));
}
#[test]
fn cs_devicecmyk_then_scn_sets_cmyk_fill() {
let bytes = b"q /DeviceCMYK cs 1 0 0 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(fill_rgb(&first_path(bytes)), (0, 255, 255));
}
#[test]
fn upper_cs_and_upper_sc_set_stroke_color() {
let bytes = b"q /DeviceRGB CS 0 1 0 SC 0 0 m 10 10 l S Q\n";
let p = first_path(bytes);
let s = p.stroke.as_ref().expect("stroke set");
match &s.paint {
Paint::Solid(c) => assert_eq!((c.r, c.g, c.b), (0, 255, 0)),
other => panic!("unexpected stroke paint: {other:?}"),
}
}
#[test]
fn pattern_scn_keeps_black_fallback() {
let bytes = b"q /Pattern cs /P0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(fill_rgb(&first_path(bytes)), (0, 0, 0));
}
fn shading_pattern(coords: [f32; 4], matrix: Option<[f32; 6]>) -> Dict {
let shading = Dict::new()
.with("ShadingType", Object::Integer(2))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with(
"Coords",
Object::Array(coords.into_iter().map(|n| Object::Real(n as f64)).collect()),
)
.with("Function", exp_black_to_white());
let mut d = Dict::new()
.with("PatternType", Object::Integer(2))
.with("Shading", Object::Dict(shading));
if let Some(m) = matrix {
d.set(
"Matrix",
Object::Array(m.into_iter().map(|n| Object::Real(n as f64)).collect()),
);
}
d
}
fn first_fill_with_pattern(bytes: &[u8], patterns: &Dict) -> Paint {
let parsed = parse_content_stream_full_with_patterns(
bytes,
None,
None,
None,
None,
None,
None,
Some(patterns),
)
.unwrap();
let Node::Group(g) = &parsed.root.children[0] else {
panic!("expected group");
};
let Node::Path(p) = &g.children[0] else {
panic!("expected path");
};
p.fill.clone().expect("path has a fill")
}
#[test]
fn shading_pattern_axial_paints_linear_gradient() {
let pat = Dict::new().with(
"P0",
Object::Dict(shading_pattern([0.0, 0.0, 100.0, 0.0], None)),
);
let bytes = b"q /Pattern cs /P0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
let Paint::LinearGradient(lg) = first_fill_with_pattern(bytes, &pat) else {
panic!("expected a linear gradient");
};
assert!((lg.start.x - 0.0).abs() < 1e-3 && (lg.start.y - 0.0).abs() < 1e-3);
assert!((lg.end.x - 100.0).abs() < 1e-3 && (lg.end.y - 0.0).abs() < 1e-3);
assert_eq!(lg.stops.len(), 64);
assert_eq!((lg.stops[0].color.r, lg.stops[0].color.g), (0, 0));
let last = lg.stops.last().unwrap();
assert_eq!((last.color.r, last.color.g, last.color.b), (255, 255, 255));
assert!((lg.stops[0].offset - 0.0).abs() < 1e-6);
assert!((last.offset - 1.0).abs() < 1e-6);
}
#[test]
fn shading_pattern_matrix_transforms_axis() {
let pat = Dict::new().with(
"P0",
Object::Dict(shading_pattern(
[0.0, 0.0, 100.0, 0.0],
Some([1.0, 0.0, 0.0, 1.0, 50.0, 20.0]),
)),
);
let bytes = b"q /Pattern cs /P0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
let Paint::LinearGradient(lg) = first_fill_with_pattern(bytes, &pat) else {
panic!("expected a linear gradient");
};
assert!((lg.start.x - 50.0).abs() < 1e-3 && (lg.start.y - 20.0).abs() < 1e-3);
assert!((lg.end.x - 150.0).abs() < 1e-3 && (lg.end.y - 20.0).abs() < 1e-3);
}
#[test]
fn shading_pattern_radial_paints_radial_gradient() {
let shading = Dict::new()
.with("ShadingType", Object::Integer(3))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with(
"Coords",
Object::Array(
[10.0, 20.0, 0.0, 10.0, 20.0, 40.0]
.into_iter()
.map(|n: f64| Object::Real(n))
.collect(),
),
)
.with("Function", exp_black_to_white());
let pat = Dict::new().with(
"P0",
Object::Dict(
Dict::new()
.with("PatternType", Object::Integer(2))
.with("Shading", Object::Dict(shading)),
),
);
let bytes = b"q /Pattern cs /P0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
let Paint::RadialGradient(rg) = first_fill_with_pattern(bytes, &pat) else {
panic!("expected a radial gradient");
};
assert!((rg.center.x - 10.0).abs() < 1e-3 && (rg.center.y - 20.0).abs() < 1e-3);
assert!((rg.radius - 40.0).abs() < 1e-3);
assert_eq!(rg.stops.len(), 64);
}
#[test]
fn tiling_pattern_keeps_black_fallback() {
let pat = Dict::new().with(
"P0",
Object::Dict(Dict::new().with("PatternType", Object::Integer(1))),
);
let bytes = b"q /Pattern cs /P0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
match first_fill_with_pattern(bytes, &pat) {
Paint::Solid(c) => assert_eq!((c.r, c.g, c.b), (0, 0, 0)),
other => panic!("expected black solid fallback, got {other:?}"),
}
}
#[test]
fn unknown_resource_colorspace_sc_keeps_black_fallback() {
let bytes = b"q /CS0 cs 0.2 0.4 0.6 sc 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(fill_rgb(&first_path(bytes)), (0, 0, 0));
}
#[test]
fn bare_cs_initialises_color_to_black() {
let bytes = b"q /DeviceRGB cs 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(fill_rgb(&first_path(bytes)), (0, 0, 0));
}
#[test]
fn switching_colorspace_reroutes_following_sc() {
let bytes = b"q /DeviceGray cs 1 sc /DeviceRGB cs 0 0 1 sc \
0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(fill_rgb(&first_path(bytes)), (0, 0, 255));
}
#[test]
fn color_space_from_name_table() {
assert_eq!(
ColorSpaceKind::from_name("DeviceGray"),
ColorSpaceKind::DeviceGray
);
assert_eq!(ColorSpaceKind::from_name("G"), ColorSpaceKind::DeviceGray);
assert_eq!(
ColorSpaceKind::from_name("DeviceRGB"),
ColorSpaceKind::DeviceRgb
);
assert_eq!(ColorSpaceKind::from_name("RGB"), ColorSpaceKind::DeviceRgb);
assert_eq!(
ColorSpaceKind::from_name("DeviceCMYK"),
ColorSpaceKind::DeviceCmyk
);
assert_eq!(
ColorSpaceKind::from_name("CMYK"),
ColorSpaceKind::DeviceCmyk
);
assert_eq!(
ColorSpaceKind::from_name("Pattern"),
ColorSpaceKind::Unknown
);
assert_eq!(ColorSpaceKind::from_name("CS0"), ColorSpaceKind::Unknown);
}
fn first_fill_with_cs(bytes: &[u8], cs: &Dict) -> (u8, u8, u8) {
let parsed =
parse_content_stream_full_with_color_space(bytes, None, None, None, Some(cs)).unwrap();
let Node::Group(g) = &parsed.root.children[0] else {
panic!("expected group");
};
let Node::Path(p) = &g.children[0] else {
panic!("expected path");
};
match &p.fill {
Some(Paint::Solid(c)) => (c.r, c.g, c.b),
other => panic!("unexpected fill: {other:?}"),
}
}
#[test]
fn icc_based_n3_resolves_devicergb() {
let arr = Object::Array(vec![
Object::Name("ICCBased".into()),
Object::Dict(Dict::new().with("N", Object::Integer(3))),
]);
assert_eq!(color_space_from_object(&arr), ColorSpaceKind::DeviceRgb);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 0 0 sc 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 0, 0));
}
#[test]
fn icc_based_n1_and_n4_resolve_gray_and_cmyk() {
let gray = Object::Array(vec![
Object::Name("ICCBased".into()),
Object::Dict(Dict::new().with("N", Object::Integer(1))),
]);
assert_eq!(color_space_from_object(&gray), ColorSpaceKind::DeviceGray);
let cmyk = Object::Array(vec![
Object::Name("ICCBased".into()),
Object::Dict(Dict::new().with("N", Object::Integer(4))),
]);
assert_eq!(color_space_from_object(&cmyk), ColorSpaceKind::DeviceCmyk);
let cs = Dict::new().with("CS0", cmyk);
let bytes = b"q /CS0 cs 1 0 0 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 255, 255));
}
#[test]
fn icc_based_alternate_wins_over_n() {
let arr = Object::Array(vec![
Object::Name("ICCBased".into()),
Object::Dict(
Dict::new()
.with("N", Object::Integer(3))
.with("Alternate", Object::Name("DeviceCMYK".into())),
),
]);
assert_eq!(color_space_from_object(&arr), ColorSpaceKind::DeviceCmyk);
}
#[test]
fn icc_based_without_n_is_unknown() {
let arr = Object::Array(vec![
Object::Name("ICCBased".into()),
Object::Dict(Dict::new()),
]);
assert_eq!(color_space_from_object(&arr), ColorSpaceKind::Unknown);
}
#[test]
fn indexed_devicergb_index_selects_table_entry() {
let table = vec![
0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, ];
let arr = Object::Array(vec![
Object::Name("Indexed".into()),
Object::Name("DeviceRGB".into()),
Object::Integer(2),
Object::HexString(table),
]);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 sc 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 0, 0));
}
#[test]
fn indexed_bare_cs_uses_entry_zero() {
let table = vec![0x10, 0x20, 0x30, 0xFF, 0xFF, 0xFF];
let arr = Object::Array(vec![
Object::Name("Indexed".into()),
Object::Name("DeviceRGB".into()),
Object::Integer(1),
Object::HexString(table),
]);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0x10, 0x20, 0x30));
}
#[test]
fn indexed_index_rounds_and_clamps() {
let table = vec![
0x00, 0x00, 0x00, 0x40, 0x40, 0x40, 0x80, 0x80, 0x80, ];
let mk = |arr| Dict::new().with("CS0", arr);
let arr = || {
Object::Array(vec![
Object::Name("Indexed".into()),
Object::Name("DeviceRGB".into()),
Object::Integer(2),
Object::HexString(table.clone()),
])
};
let bytes = b"q /CS0 cs 1.6 sc 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &mk(arr())), (0x80, 0x80, 0x80));
let bytes = b"q /CS0 cs 9 sc 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &mk(arr())), (0x80, 0x80, 0x80));
let bytes = b"q /CS0 cs -3 sc 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &mk(arr())), (0x00, 0x00, 0x00));
}
#[test]
fn indexed_nondevice_base_is_unknown() {
let arr = Object::Array(vec![
Object::Name("Indexed".into()),
Object::Name("Lab".into()),
Object::Integer(1),
Object::HexString(vec![0, 0, 0, 1, 1, 1]),
]);
assert_eq!(color_space_from_object(&arr), ColorSpaceKind::Unknown);
}
#[test]
fn indexed_truncated_table_returns_none_for_missing_slot() {
let table = vec![0x00, 0x00, 0x00];
let base = ColorSpaceKind::DeviceRgb;
assert!(indexed_color(&base, 2, &table, 0.0).is_some());
assert!(indexed_color(&base, 2, &table, 2.0).is_none());
let arr = Object::Array(vec![
Object::Name("Indexed".into()),
Object::Name("DeviceRGB".into()),
Object::Integer(2),
Object::HexString(table),
]);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 2 sc 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 0, 0));
}
#[test]
fn device_name_resolves_without_consulting_resources() {
let cs = Dict::new().with("DeviceRGB", Object::Name("DeviceGray".into()));
let bytes = b"q /DeviceRGB cs 1 0 0 sc 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 0, 0));
}
#[test]
fn resource_key_without_resources_stays_unknown() {
assert_eq!(
ColorSpaceKind::resolve_with_resources("CS0", None),
ColorSpaceKind::Unknown
);
}
fn num_arr(vals: &[f32]) -> Object {
Object::Array(vals.iter().map(|v| Object::Real(*v as f64)).collect())
}
fn type2(c0: &[f32], c1: &[f32], n: f32) -> Object {
Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(2))
.with("Domain", num_arr(&[0.0, 1.0]))
.with("C0", num_arr(c0))
.with("C1", num_arr(c1))
.with("N", Object::Real(n as f64)),
)
}
#[test]
fn type2_exponential_interpolates() {
let f = PdfFunction::parse(&type2(&[0.0, 0.0, 0.0, 0.0], &[1.0, 0.0, 0.0, 0.0], 1.0))
.expect("type 2 parses");
assert_eq!(f.eval(0.0), vec![0.0, 0.0, 0.0, 0.0]);
assert_eq!(f.eval(1.0), vec![1.0, 0.0, 0.0, 0.0]);
assert_eq!(f.eval(0.5), vec![0.5, 0.0, 0.0, 0.0]);
}
#[test]
fn type2_exponent_two_is_quadratic() {
let f = PdfFunction::parse(&type2(&[0.0], &[1.0], 2.0)).expect("parses");
assert!((f.eval(0.5)[0] - 0.25).abs() < 1e-6);
}
#[test]
fn type2_range_clips_output() {
let dict = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(2))
.with("Domain", num_arr(&[0.0, 1.0]))
.with("Range", num_arr(&[0.0, 1.0]))
.with("C0", num_arr(&[0.0]))
.with("C1", num_arr(&[2.0]))
.with("N", Object::Integer(1)),
);
let f = PdfFunction::parse(&dict).expect("parses");
assert_eq!(f.eval(1.0), vec![1.0]);
}
#[test]
fn type3_stitching_routes_to_subdomain() {
let dict = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(3))
.with("Domain", num_arr(&[0.0, 1.0]))
.with(
"Functions",
Object::Array(vec![type2(&[0.0], &[1.0], 1.0), type2(&[1.0], &[0.0], 1.0)]),
)
.with("Bounds", num_arr(&[0.5]))
.with("Encode", num_arr(&[0.0, 1.0, 0.0, 1.0])),
);
let f = PdfFunction::parse(&dict).expect("type 3 parses");
assert!((f.eval(0.25)[0] - 0.5).abs() < 1e-6);
assert!((f.eval(0.75)[0] - 0.5).abs() < 1e-6);
assert!(f.eval(1.0)[0].abs() < 1e-6);
}
#[test]
fn type0_without_samples_and_type4_without_program_are_not_evaluable() {
let t0 = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 1.0]))
.with("Range", num_arr(&[0.0, 1.0]))
.with("Size", num_arr(&[2.0]))
.with("BitsPerSample", Object::Integer(8)),
);
assert!(PdfFunction::parse(&t0).is_none());
let t4 = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(4))
.with("Domain", num_arr(&[0.0, 1.0]))
.with("Range", num_arr(&[0.0, 1.0])),
);
assert!(PdfFunction::parse(&t4).is_none());
}
fn type0_8bit(domain: &[f32], range: &[f32], codes: &[u8]) -> Object {
Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(domain))
.with("Range", num_arr(range))
.with("Size", num_arr(&[codes.len() as f32]))
.with("BitsPerSample", Object::Integer(8))
.with("__Samples", Object::HexString(codes.to_vec())),
)
}
#[test]
fn type0_8bit_linear_identity() {
let f = PdfFunction::parse(&type0_8bit(&[0.0, 1.0], &[0.0, 1.0], &[0, 255]))
.expect("type0 parses");
assert!((f.eval(0.0)[0] - 0.0).abs() < 1e-6);
assert!((f.eval(1.0)[0] - 1.0).abs() < 1e-6);
assert!((f.eval(0.5)[0] - 0.5).abs() < 1e-6);
}
#[test]
fn type0_decode_remaps_outputs() {
let f = PdfFunction::parse(&Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 1.0]))
.with("Range", num_arr(&[0.0, 10.0]))
.with("Size", num_arr(&[2.0]))
.with("BitsPerSample", Object::Integer(8))
.with("Decode", num_arr(&[0.0, 10.0]))
.with("__Samples", Object::HexString(vec![0, 255])),
))
.expect("type0 parses");
assert!((f.eval(0.0)[0] - 0.0).abs() < 1e-5);
assert!((f.eval(1.0)[0] - 10.0).abs() < 1e-5);
assert!((f.eval(0.5)[0] - 5.0).abs() < 1e-5);
}
#[test]
fn type0_single_sample_multi_output() {
let f = PdfFunction::parse(&Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 1.0]))
.with("Range", num_arr(&[0.0, 1.0, 0.0, 1.0]))
.with("Size", num_arr(&[1.0]))
.with("BitsPerSample", Object::Integer(8))
.with("__Samples", Object::HexString(vec![255, 0])),
))
.expect("type0 parses");
let out = f.eval(0.42);
assert_eq!(out.len(), 2);
assert!((out[0] - 1.0).abs() < 1e-5);
assert!((out[1] - 0.0).abs() < 1e-5);
}
#[test]
fn type0_1bit_packing_msb_first() {
let f = PdfFunction::parse(&Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 1.0]))
.with("Range", num_arr(&[0.0, 1.0]))
.with("Size", num_arr(&[4.0]))
.with("BitsPerSample", Object::Integer(1))
.with("Encode", num_arr(&[0.0, 3.0]))
.with("__Samples", Object::HexString(vec![0b1010_0000])),
))
.expect("type0 parses");
assert!((f.eval(0.0)[0] - 1.0).abs() < 1e-6);
assert!((f.eval(1.0 / 3.0)[0] - 0.0).abs() < 1e-5);
assert!((f.eval(2.0 / 3.0)[0] - 1.0).abs() < 1e-5);
}
#[test]
fn type0_bilinear_2x2_grid() {
let t0 = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 1.0, 0.0, 1.0]))
.with("Range", num_arr(&[0.0, 1.0]))
.with("Size", num_arr(&[2.0, 2.0]))
.with("BitsPerSample", Object::Integer(8))
.with("__Samples", Object::HexString(vec![0, 255, 128, 64])),
);
let f = PdfFunction::parse(&t0).expect("2-input type0 parses");
assert!((f.eval_n(&[0.0, 0.0])[0] - 0.0).abs() < 1e-6);
assert!((f.eval_n(&[1.0, 0.0])[0] - 1.0).abs() < 1e-6);
assert!((f.eval_n(&[0.0, 1.0])[0] - 128.0 / 255.0).abs() < 1e-6);
assert!((f.eval_n(&[1.0, 1.0])[0] - 64.0 / 255.0).abs() < 1e-6);
let mean = (0.0 + 255.0 + 128.0 + 64.0) / 4.0 / 255.0;
assert!((f.eval_n(&[0.5, 0.5])[0] - mean).abs() < 1e-6);
assert!((f.eval_n(&[0.5, 0.0])[0] - 0.5).abs() < 1e-6);
}
#[test]
fn type0_order_3_passes_through_knots() {
let t0 = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 3.0]))
.with("Range", num_arr(&[0.0, 1.0]))
.with("Size", num_arr(&[4.0]))
.with("BitsPerSample", Object::Integer(8))
.with("Order", Object::Integer(3))
.with("__Samples", Object::HexString(vec![0, 85, 170, 255])),
);
let f = PdfFunction::parse(&t0).expect("order-3 sampled function parses");
let expect = [0.0, 85.0 / 255.0, 170.0 / 255.0, 1.0];
for (k, &e) in expect.iter().enumerate() {
let got = f.eval_n(&[k as f32])[0];
assert!((got - e).abs() < 1e-6, "knot {k}: got {got}, want {e}");
}
}
#[test]
fn type0_order_3_constant_table_is_flat() {
let t0 = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 3.0]))
.with("Range", num_arr(&[0.0, 1.0]))
.with("Size", num_arr(&[4.0]))
.with("BitsPerSample", Object::Integer(8))
.with("Order", Object::Integer(3))
.with("__Samples", Object::HexString(vec![128, 128, 128, 128])),
);
let f = PdfFunction::parse(&t0).expect("parses");
for &x in &[0.0f32, 0.3, 1.0, 1.7, 2.5, 3.0] {
let got = f.eval_n(&[x])[0];
assert!((got - 128.0 / 255.0).abs() < 1e-6, "x={x}: got {got}");
}
}
#[test]
fn type0_order_3_falls_back_to_linear_below_size_4() {
let t0 = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 1.0]))
.with("Range", num_arr(&[0.0, 1.0]))
.with("Size", num_arr(&[2.0]))
.with("BitsPerSample", Object::Integer(8))
.with("Order", Object::Integer(3))
.with("__Samples", Object::HexString(vec![0, 255])),
);
let f = PdfFunction::parse(&t0).expect("parses");
assert!((f.eval_n(&[0.5])[0] - 0.5).abs() < 1e-6);
assert!((f.eval_n(&[0.0])[0] - 0.0).abs() < 1e-6);
assert!((f.eval_n(&[1.0])[0] - 1.0).abs() < 1e-6);
}
#[test]
fn type0_invalid_order_is_rejected() {
let t0 = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 1.0]))
.with("Range", num_arr(&[0.0, 1.0]))
.with("Size", num_arr(&[4.0]))
.with("BitsPerSample", Object::Integer(8))
.with("Order", Object::Integer(2))
.with("__Samples", Object::HexString(vec![0, 85, 170, 255])),
);
assert!(PdfFunction::parse(&t0).is_none());
}
#[test]
fn type0_default_order_is_linear() {
let t0 = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 3.0]))
.with("Range", num_arr(&[0.0, 1.0]))
.with("Size", num_arr(&[4.0]))
.with("BitsPerSample", Object::Integer(8))
.with("__Samples", Object::HexString(vec![0, 85, 170, 255])),
);
let f = PdfFunction::parse(&t0).expect("parses");
let mid = (85.0 + 170.0) / 2.0 / 255.0;
assert!((f.eval_n(&[1.5])[0] - mid).abs() < 1e-6);
}
fn type4(domain: &[f32], range: &[f32], src: &str) -> Object {
Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(4))
.with("Domain", num_arr(domain))
.with("Range", num_arr(range))
.with("__Program", Object::HexString(src.as_bytes().to_vec())),
)
}
#[test]
fn type4_identity_program() {
let f = PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0], "{ }"))
.expect("type4 identity parses");
assert!((f.eval(0.25)[0] - 0.25).abs() < 1e-6);
assert!((f.eval(0.9)[0] - 0.9).abs() < 1e-6);
}
#[test]
fn type4_arithmetic_mul_and_range_clip() {
let f = PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0], "{ 2 mul }")).expect("parses");
assert!((f.eval(0.25)[0] - 0.5).abs() < 1e-6);
assert!((f.eval(0.8)[0] - 1.0).abs() < 1e-6);
}
#[test]
fn type4_invert_with_exch_sub() {
let f =
PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0], "{ 1 exch sub }")).expect("parses");
assert!((f.eval(0.0)[0] - 1.0).abs() < 1e-6);
assert!((f.eval(1.0)[0] - 0.0).abs() < 1e-6);
assert!((f.eval(0.3)[0] - 0.7).abs() < 1e-6);
}
#[test]
fn type4_dup_emits_two_outputs() {
let f = PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0, 0.0, 1.0], "{ dup }"))
.expect("parses");
let out = f.eval(0.4);
assert_eq!(out.len(), 2);
assert!((out[0] - 0.4).abs() < 1e-6);
assert!((out[1] - 0.4).abs() < 1e-6);
}
#[test]
fn type4_ifelse_threshold() {
let f = PdfFunction::parse(&type4(
&[0.0, 1.0],
&[0.0, 1.0],
"{ 0.5 ge { 1 } { 0 } ifelse }",
))
.expect("parses");
assert!((f.eval(0.2)[0] - 0.0).abs() < 1e-6);
assert!((f.eval(0.5)[0] - 1.0).abs() < 1e-6);
assert!((f.eval(0.9)[0] - 1.0).abs() < 1e-6);
}
#[test]
fn type4_single_branch_if() {
let f = PdfFunction::parse(&type4(
&[0.0, 1.0],
&[0.0, 1.0],
"{ dup 0 lt { pop 0 } if }",
))
.expect("parses");
assert!((f.eval(0.6)[0] - 0.6).abs() < 1e-6);
}
#[test]
fn type4_roll_rotates_stack() {
let f = PdfFunction::parse(&type4(
&[0.0, 100.0],
&[0.0, 100.0, 0.0, 100.0, 0.0, 100.0],
"{ 10 20 3 1 roll }",
))
.expect("parses");
let out = f.eval(5.0);
assert_eq!(out.len(), 3);
assert_eq!((out[0], out[1], out[2]), (20.0, 5.0, 10.0));
}
#[test]
fn type4_index_copies_nth() {
let f = PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0, 0.0, 1.0], "{ 0 index }"))
.expect("parses");
let out = f.eval(0.7);
assert_eq!(out.len(), 2);
assert!((out[0] - 0.7).abs() < 1e-6 && (out[1] - 0.7).abs() < 1e-6);
}
#[test]
fn type4_boolean_not_and_relational() {
let f = PdfFunction::parse(&type4(
&[0.0, 1.0],
&[0.0, 1.0],
"{ 0.5 gt not { 0 } { 1 } ifelse }",
))
.expect("parses");
assert!((f.eval(0.9)[0] - 1.0).abs() < 1e-6);
assert!((f.eval(0.2)[0] - 0.0).abs() < 1e-6);
}
#[test]
fn type4_division_by_zero_falls_back_to_black() {
let f = PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0], "{ 0 div }")).expect("parses");
assert_eq!(f.eval(0.5), vec![0.0]);
}
#[test]
fn type4_output_arity_mismatch_falls_back() {
let f = PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0], "{ pop }")).expect("parses");
assert_eq!(f.eval(0.5), vec![0.0]);
}
#[test]
fn type4_syntax_errors_reject() {
assert!(PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0], "2 mul")).is_none());
assert!(PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0], "{ 2 mul")).is_none());
assert!(PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0], "{ } 3")).is_none());
assert!(PdfFunction::parse(&type4(&[0.0, 1.0], &[0.0, 1.0], "{ frobnicate }")).is_none());
}
#[test]
fn type4_separation_scn_end_to_end() {
let arr = separation(
"Spot",
Object::Name("DeviceGray".into()),
type4(&[0.0, 1.0], &[0.0, 1.0], "{ 1 exch sub }"),
);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 0, 0));
let bytes = b"q /CS0 cs 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 255, 255));
}
fn device_n(names: &[&str], alt: Object, tint: Object) -> Object {
Object::Array(vec![
Object::Name("DeviceN".into()),
Object::Array(names.iter().map(|n| Object::Name((*n).into())).collect()),
alt,
tint,
])
}
#[test]
fn device_n_duotone_type4_maps_to_rgb() {
let tint = type4(
&[0.0, 1.0, 0.0, 1.0],
&[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
"{ 0 exch }",
);
let arr = device_n(&["Red", "Blue"], Object::Name("DeviceRGB".into()), tint);
assert!(matches!(
color_space_from_object(&arr),
ColorSpaceKind::DeviceN { n_in: 2, .. }
));
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 0.5 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 0, 128));
}
#[test]
fn device_n_type0_sampled_bilinear_to_gray() {
let tint = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(0))
.with("Domain", num_arr(&[0.0, 1.0, 0.0, 1.0]))
.with("Range", num_arr(&[0.0, 1.0]))
.with("Size", num_arr(&[2.0, 2.0]))
.with("BitsPerSample", Object::Integer(8))
.with("__Samples", Object::HexString(vec![0, 255, 255, 255])),
);
let arr = device_n(&["A", "B"], Object::Name("DeviceGray".into()), tint);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 255, 255));
let bytes = b"q /CS0 cs 0 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 0, 0));
}
#[test]
fn device_n_bare_cs_uses_full_tint() {
let tint = type4(
&[0.0, 1.0, 0.0, 1.0],
&[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
"{ 0 exch }",
);
let arr = device_n(&["Red", "Blue"], Object::Name("DeviceRGB".into()), tint);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 0, 255));
}
#[test]
fn device_n_all_none_discards_output() {
let tint = type4(
&[0.0, 1.0, 0.0, 1.0],
&[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
"{ 0 exch }",
);
let arr = device_n(&["None", "None"], Object::Name("DeviceRGB".into()), tint);
assert!(matches!(
color_space_from_object(&arr),
ColorSpaceKind::DeviceN { all_none: true, .. }
));
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 1 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 0, 0));
}
#[test]
fn device_n_arity_mismatch_falls_back() {
let tint = type4(&[0.0, 1.0, 0.0, 1.0], &[0.0, 1.0], "{ add }");
let arr = device_n(&["A", "B", "C"], Object::Name("DeviceGray".into()), tint);
assert_eq!(color_space_from_object(&arr), ColorSpaceKind::Unknown);
}
#[test]
fn device_n_nondevice_alternate_falls_back() {
let tint = type4(&[0.0, 1.0, 0.0, 1.0], &[0.0, 1.0], "{ add }");
let arr = device_n(&["A", "B"], Object::Name("Pattern".into()), tint);
assert_eq!(color_space_from_object(&arr), ColorSpaceKind::Unknown);
}
#[test]
fn type4_parses_doubledot_example() {
let prog =
parse_ps_program(b"{ 360 mul sin 2 div exch 360 mul sin 2 div add }").expect("parses");
assert_eq!(prog.first(), Some(&PsToken::Number(360.0)));
assert_eq!(prog.get(1), Some(&PsToken::Op(PsOp::Mul)));
assert_eq!(prog.last(), Some(&PsToken::Op(PsOp::Add)));
}
fn separation(name: &str, alt: Object, tint: Object) -> Object {
Object::Array(vec![
Object::Name("Separation".into()),
Object::Name(name.into()),
alt,
tint,
])
}
#[test]
fn separation_cmyk_tint_maps_through_alternate() {
let tint = type2(&[0.0, 0.0, 0.0, 0.0], &[1.0, 0.0, 0.0, 0.0], 1.0);
let arr = separation("LogoGreen", Object::Name("DeviceCMYK".into()), tint);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 255, 255));
let bytes = b"q /CS0 cs 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 255, 255));
}
#[test]
fn separation_bare_cs_uses_full_tint() {
let tint = type2(&[1.0], &[0.0], 1.0); let arr = separation("Spot", Object::Name("DeviceGray".into()), tint);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 0, 0));
let bytes = b"q /CS0 cs 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 255, 255));
}
#[test]
fn separation_with_type3_tint() {
let tint = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(3))
.with("Domain", num_arr(&[0.0, 1.0]))
.with(
"Functions",
Object::Array(vec![type2(&[0.0], &[0.5], 1.0), type2(&[0.5], &[1.0], 1.0)]),
)
.with("Bounds", num_arr(&[0.5]))
.with("Encode", num_arr(&[0.0, 1.0, 0.0, 1.0])),
);
let arr = separation("Spot", Object::Name("DeviceGray".into()), tint);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 0.75 scn 0 0 m 10 10 l 10 0 l h f Q\n";
let (r, g, b) = first_fill_with_cs(bytes, &cs);
let expect = (0.75f32 * 255.0).round() as u8;
assert_eq!((r, g, b), (expect, expect, expect));
}
#[test]
fn separation_with_type0_tint() {
let tint = type0_8bit(&[0.0, 1.0], &[0.0, 1.0], &[255, 0]);
let arr = separation("Spot", Object::Name("DeviceGray".into()), tint);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 0, 0));
let bytes = b"q /CS0 cs 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 255, 255));
}
#[test]
fn separation_none_colorant_produces_no_paint() {
let arr = separation(
"None",
Object::Name("DeviceGray".into()),
type2(&[0.0], &[1.0], 1.0),
);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 0.5 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 0, 0));
}
#[test]
fn separation_nondevice_alternate_is_unknown() {
let arr = separation(
"Spot",
Object::Name("Lab".into()),
type2(&[0.0], &[1.0], 1.0),
);
assert_eq!(color_space_from_object(&arr), ColorSpaceKind::Unknown);
}
#[test]
fn separation_unevaluable_tint_is_unknown() {
let t4 = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(4))
.with("Domain", num_arr(&[0.0, 1.0]))
.with("Range", num_arr(&[0.0, 1.0])),
);
let arr = separation("Spot", Object::Name("DeviceGray".into()), t4);
assert_eq!(color_space_from_object(&arr), ColorSpaceKind::Unknown);
}
#[test]
fn separation_tint_clamped_to_unit_range() {
let tint = type2(&[0.0], &[1.0], 1.0); let arr = separation("Spot", Object::Name("DeviceGray".into()), tint);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 5 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 255, 255));
}
fn ext_gstate_with(name: &str, dict: Dict) -> Dict {
Dict::new().with(name, Object::Dict(dict))
}
fn parse_with(input: &[u8], ext: &Dict) -> Group {
parse_content_stream_with_resources(input, Some(ext)).unwrap()
}
#[test]
fn gs_applies_line_width_lw() {
let ext = ext_gstate_with(
"GS1",
Dict::new()
.with("Type", Object::Name("ExtGState".into()))
.with("LW", Object::Real(3.5)),
);
let bytes = b"q /GS1 gs 0 0 m 10 10 l S Q\n";
let root = parse_with(bytes, &ext);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let s = p.stroke.as_ref().expect("stroke set");
assert!((s.width - 3.5).abs() < 1e-3);
}
#[test]
fn gs_applies_lc_lj_ml() {
let ext = ext_gstate_with(
"GS1",
Dict::new()
.with("LC", Object::Integer(1)) .with("LJ", Object::Integer(2)) .with("ML", Object::Real(7.5)),
);
let bytes = b"q /GS1 gs 0 0 m 10 10 l S Q\n";
let root = parse_with(bytes, &ext);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let s = p.stroke.as_ref().expect("stroke set");
assert!(matches!(s.cap, LineCap::Round));
assert!(matches!(s.join, LineJoin::Bevel));
assert!((s.miter_limit - 7.5).abs() < 1e-3);
}
#[test]
fn gs_applies_d_dash_pattern() {
let ext = ext_gstate_with(
"GS1",
Dict::new().with(
"D",
Object::Array(vec![
Object::Array(vec![Object::Real(4.0), Object::Real(2.0)]),
Object::Real(1.0),
]),
),
);
let bytes = b"q /GS1 gs 0 0 m 10 10 l S Q\n";
let root = parse_with(bytes, &ext);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let s = p.stroke.as_ref().expect("stroke set");
let dash = s.dash.as_ref().expect("dash set");
assert_eq!(dash.array, vec![4.0, 2.0]);
assert!((dash.offset - 1.0).abs() < 1e-3);
}
#[test]
fn gs_applies_ca_to_fill_alpha() {
let ext = ext_gstate_with("GS1", Dict::new().with("ca", Object::Real(0.5)));
let bytes = b"q 1 0 0 rg /GS1 gs 0 0 m 10 10 l 10 0 l h f Q\n";
let root = parse_with(bytes, &ext);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let Some(Paint::Solid(c)) = &p.fill else {
panic!("fill")
};
assert_eq!((c.r, c.g, c.b), (255, 0, 0));
assert_eq!(c.a, 128);
}
#[test]
fn gs_applies_cap_ca_to_stroke_alpha() {
let ext = ext_gstate_with("GS1", Dict::new().with("CA", Object::Real(0.25)));
let bytes = b"q 0 1 0 RG /GS1 gs 0 0 m 10 10 l S Q\n";
let root = parse_with(bytes, &ext);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let s = p.stroke.as_ref().expect("stroke set");
let Paint::Solid(c) = &s.paint else { panic!() };
assert_eq!((c.r, c.g, c.b), (0, 255, 0));
assert_eq!(c.a, 64);
}
#[test]
fn gs_unknown_name_is_no_op() {
let ext = ext_gstate_with("GS1", Dict::new().with("LW", Object::Real(9.0)));
let bytes = b"q 2.5 w /GS_OTHER gs 0 0 m 10 10 l S Q\n";
let root = parse_with(bytes, &ext);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let s = p.stroke.as_ref().expect("stroke");
assert!((s.width - 2.5).abs() < 1e-3);
}
#[test]
fn multiple_gs_invocations_cumulate() {
let mut ext = Dict::new();
ext.set(
"GW",
Object::Dict(Dict::new().with("LW", Object::Real(4.0))),
);
ext.set(
"GA",
Object::Dict(Dict::new().with("CA", Object::Real(0.5))),
);
let bytes = b"q /GW gs /GA gs 1 0 0 RG 0 0 m 10 10 l S Q\n";
let root = parse_with(bytes, &ext);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let s = p.stroke.as_ref().expect("stroke");
assert!((s.width - 4.0).abs() < 1e-3);
let Paint::Solid(c) = &s.paint else { panic!() };
assert_eq!(c.a, 128);
}
#[test]
fn legacy_parse_content_stream_drops_gs_operands() {
let bytes = b"q 2.5 w /GS1 gs 0 0 m 10 10 l S Q\n";
let root = parse_content_stream(bytes).unwrap();
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let s = p.stroke.as_ref().expect("stroke");
assert!((s.width - 2.5).abs() < 1e-3);
}
#[test]
fn gs_unknown_table_58_keys_are_tolerated() {
let ext = ext_gstate_with(
"GS1",
Dict::new()
.with("BM", Object::Name("Multiply".into()))
.with("OP", Object::Bool(true))
.with("RI", Object::Name("Perceptual".into()))
.with("LW", Object::Real(2.0)),
);
let bytes = b"q /GS1 gs 0 0 m 10 10 l S Q\n";
let root = parse_with(bytes, &ext);
let Node::Group(g) = &root.children[0] else {
panic!()
};
let Node::Path(p) = &g.children[0] else {
panic!()
};
let s = p.stroke.as_ref().expect("stroke set");
assert!((s.width - 2.0).abs() < 1e-3);
}
#[test]
fn apply_alpha_composes_with_existing_alpha() {
let base = Paint::Solid(Rgba::new(100, 200, 50, 200));
let out = apply_alpha(base, 0.5);
let Paint::Solid(c) = out else { panic!() };
assert_eq!((c.r, c.g, c.b), (100, 200, 50));
assert_eq!(c.a, 100);
}
#[test]
fn apply_alpha_unit_is_identity() {
let base = Paint::Solid(Rgba::new(10, 20, 30, 200));
let out = apply_alpha(base, 1.0);
let Paint::Solid(c) = out else { panic!() };
assert_eq!(c.a, 200);
}
#[test]
fn parse_dash_pair_two_element_array() {
let obj = Object::Array(vec![
Object::Array(vec![Object::Real(2.0), Object::Real(1.0)]),
Object::Integer(3),
]);
let (arr, off) = parse_dash_pair(&obj).expect("parses");
assert_eq!(arr, vec![2.0, 1.0]);
assert!((off - 3.0).abs() < 1e-3);
}
#[test]
fn parse_dash_pair_rejects_malformed() {
assert!(parse_dash_pair(&Object::Integer(0)).is_none());
assert!(parse_dash_pair(&Object::Array(vec![Object::Integer(0)])).is_none());
assert!(
parse_dash_pair(&Object::Array(vec![Object::Integer(0), Object::Integer(0)])).is_none()
);
}
fn font_res_with(name: &str, dict: Dict) -> Dict {
Dict::new().with(name, Object::Dict(dict))
}
fn parse_full(input: &[u8], ext: Option<&Dict>, fonts: Option<&Dict>) -> ParsedContent {
parse_content_stream_full(input, ext, fonts).unwrap()
}
#[test]
fn tj_emits_one_text_show_with_font_and_size() {
let f1 = Dict::new()
.with("Type", Object::Name("Font".into()))
.with("Subtype", Object::Name("Type1".into()))
.with("BaseFont", Object::Name("Helvetica".into()));
let fonts = font_res_with("F1", f1);
let bytes = b"BT /F1 12 Tf 72 712 Td (Hello) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 1);
let show = &p.text_shows[0];
assert_eq!(show.font_name, "F1");
assert!((show.font_size - 12.0).abs() < 1e-3);
assert_eq!(show.bytes, b"Hello");
assert!((show.position.0 - 72.0).abs() < 1e-3);
assert!((show.position.1 - 712.0).abs() < 1e-3);
assert!(matches!(show.operator, TextShowOp::Tj));
assert!(show.font_dict.is_some());
}
#[test]
fn tj_array_concatenates_strings_and_drops_kerns() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"BT /F1 10 Tf 0 0 Td [(Hel) -250 (lo) -120 (!)] TJ ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 1);
let show = &p.text_shows[0];
assert_eq!(show.bytes, b"Hello!");
assert!(matches!(show.operator, TextShowOp::TJ));
}
#[test]
fn single_quote_does_implicit_t_star_then_show() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"BT /F1 12 Tf 14 TL 0 100 Td (first) Tj (second) ' ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 2);
assert_eq!(p.text_shows[0].bytes, b"first");
assert!((p.text_shows[0].position.1 - 100.0).abs() < 1e-3);
assert_eq!(p.text_shows[1].bytes, b"second");
assert!((p.text_shows[1].position.1 - 86.0).abs() < 1e-3);
assert!(matches!(p.text_shows[1].operator, TextShowOp::SingleQuote));
}
#[test]
fn double_quote_does_implicit_t_star_then_show() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"BT /F1 12 Tf 10 TL 0 100 Td (first) Tj 1 2 (second) \" ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 2);
assert_eq!(p.text_shows[1].bytes, b"second");
assert!((p.text_shows[1].position.1 - 90.0).abs() < 1e-3);
assert!(matches!(p.text_shows[1].operator, TextShowOp::DoubleQuote));
}
#[test]
fn tm_sets_text_matrix_directly() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"BT /F1 10 Tf 1 0 0 1 50 600 Tm (P) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 1);
assert!((p.text_shows[0].position.0 - 50.0).abs() < 1e-3);
assert!((p.text_shows[0].position.1 - 600.0).abs() < 1e-3);
}
#[test]
fn bt_resets_text_matrix() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"BT /F1 12 Tf 100 200 Td (A) Tj ET BT /F1 12 Tf 0 0 Td (B) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 2);
assert!((p.text_shows[0].position.0 - 100.0).abs() < 1e-3);
assert!(p.text_shows[1].position.0.abs() < 1e-3);
assert!(p.text_shows[1].position.1.abs() < 1e-3);
}
#[test]
fn tj_with_unknown_font_name_still_emits_show_with_none_dict() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"BT /F_OTHER 12 Tf 0 0 Td (Hi) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 1);
assert_eq!(p.text_shows[0].font_name, "F_OTHER");
assert_eq!(p.text_shows[0].bytes, b"Hi");
assert!(p.text_shows[0].font_dict.is_none());
}
#[test]
fn legacy_entry_points_drop_tj_silently() {
let bytes = b"BT /F1 12 Tf 0 0 Td (Hello) Tj ET\n";
let r1 = parse_content_stream(bytes).unwrap();
assert!(r1.children.is_empty());
let r2 = parse_content_stream_with_resources(bytes, None).unwrap();
assert!(r2.children.is_empty());
}
#[test]
fn tj_outside_text_object_is_dropped() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"(stray) Tj\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 0);
}
#[test]
fn hex_string_operand_decodes_for_tj() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"BT /F1 12 Tf 0 0 Td <48656C6C6F> Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 1);
assert_eq!(p.text_shows[0].bytes, b"Hello");
}
#[test]
fn literal_string_octal_escape() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"BT /F1 12 Tf 0 0 Td (\\101\\102\\103) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows[0].bytes, b"ABC");
}
#[test]
fn literal_string_named_escapes() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"BT /F1 12 Tf 0 0 Td (a\\nb\\tc\\(d\\)) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows[0].bytes, b"a\nb\tc(d)");
}
#[test]
fn text_and_path_coexist_in_one_stream() {
let fonts = font_res_with("F1", Dict::new());
let bytes = b"q 0 0 m 10 10 l 10 0 l h f BT /F1 12 Tf 0 0 Td (X) Tj ET Q\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.root.children.len(), 1);
let Node::Group(g) = &p.root.children[0] else {
panic!()
};
assert!(matches!(g.children[0], Node::Path(_)));
assert_eq!(p.text_shows.len(), 1);
assert_eq!(p.text_shows[0].bytes, b"X");
}
#[test]
fn hex_string_pads_trailing_odd_nibble() {
let (end, bytes) = read_hex_string(b"<4>x", 0).unwrap();
assert_eq!(end, 3);
assert_eq!(bytes, vec![0x40]);
}
#[test]
fn hex_string_skips_whitespace_and_is_case_insensitive() {
let (_end, bytes) = read_hex_string(b"<4a 5C>", 0).unwrap();
assert_eq!(bytes, vec![0x4A, 0x5C]);
}
fn shading_res_with(name: &str, dict: Dict) -> Dict {
Dict::new().with(name, Object::Dict(dict))
}
fn parse_with_shading(
input: &[u8],
ext: Option<&Dict>,
fonts: Option<&Dict>,
shadings: Option<&Dict>,
) -> ParsedContent {
parse_content_stream_full_with_shading(input, ext, fonts, shadings).unwrap()
}
#[test]
fn sh_emits_one_shading_event_with_resolved_dict() {
let sh1 = Dict::new()
.with("ShadingType", Object::Integer(2))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with(
"Coords",
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(100.0),
Object::Real(0.0),
]),
);
let shadings = shading_res_with("Sh1", sh1);
let bytes = b"q /Sh1 sh Q\n";
let p = parse_with_shading(bytes, None, None, Some(&shadings));
assert_eq!(p.shadings.len(), 1);
let s = &p.shadings[0];
assert_eq!(s.name, "Sh1");
let dict = s.shading_dict.as_ref().expect("resolved");
let st = dict
.entries()
.iter()
.find(|(k, _)| k == "ShadingType")
.map(|(_, v)| v.clone());
assert!(matches!(st, Some(Object::Integer(2))));
assert!((s.ctm.a - 1.0).abs() < 1e-6);
assert!((s.ctm.d - 1.0).abs() < 1e-6);
assert!(s.ctm.b.abs() < 1e-6);
assert!(s.ctm.c.abs() < 1e-6);
assert!(s.ctm.e.abs() < 1e-6);
assert!(s.ctm.f.abs() < 1e-6);
assert!(s.clip.is_none());
}
#[test]
fn sh_captures_effective_ctm_from_cm() {
let sh1 = Dict::new().with("ShadingType", Object::Integer(2));
let shadings = shading_res_with("Sh1", sh1);
let bytes = b"q 27.7843 0.0 0.0 -27.7843 310.2461 121.1521 cm /Sh1 sh Q\n";
let p = parse_with_shading(bytes, None, None, Some(&shadings));
assert_eq!(p.shadings.len(), 1);
let s = &p.shadings[0];
assert!((s.ctm.a - 27.7843).abs() < 1e-3);
assert!(s.ctm.b.abs() < 1e-3);
assert!(s.ctm.c.abs() < 1e-3);
assert!((s.ctm.d - -27.7843).abs() < 1e-3);
assert!((s.ctm.e - 310.2461).abs() < 1e-3);
assert!((s.ctm.f - 121.1521).abs() < 1e-3);
}
#[test]
fn sh_composes_nested_cm_across_q_frames() {
let sh1 = Dict::new();
let shadings = shading_res_with("Sh1", sh1);
let bytes = b"q 1 0 0 1 10 20 cm q 1 0 0 1 5 0 cm /Sh1 sh Q Q\n";
let p = parse_with_shading(bytes, None, None, Some(&shadings));
assert_eq!(p.shadings.len(), 1);
let s = &p.shadings[0];
assert!((s.ctm.e - 15.0).abs() < 1e-3);
assert!((s.ctm.f - 20.0).abs() < 1e-3);
assert!((s.ctm.a - 1.0).abs() < 1e-3);
assert!((s.ctm.d - 1.0).abs() < 1e-3);
}
#[test]
fn sh_captures_active_clip_path() {
let sh1 = Dict::new();
let shadings = shading_res_with("Sh1", sh1);
let bytes = b"q 0 0 100 50 re W n /Sh1 sh Q\n";
let p = parse_with_shading(bytes, None, None, Some(&shadings));
assert_eq!(p.shadings.len(), 1);
let s = &p.shadings[0];
let clip = s.clip.as_ref().expect("clip in force");
assert!(!clip.commands.is_empty());
}
#[test]
fn sh_with_unknown_name_still_emits_event_with_none_dict() {
let shadings = shading_res_with("Sh1", Dict::new());
let bytes = b"q /Other sh Q\n";
let p = parse_with_shading(bytes, None, None, Some(&shadings));
assert_eq!(p.shadings.len(), 1);
let s = &p.shadings[0];
assert_eq!(s.name, "Other");
assert!(s.shading_dict.is_none());
}
#[test]
fn sh_without_shading_resources_emits_event_with_none_dict() {
let bytes = b"q 1 0 0 1 50 60 cm /Sh1 sh Q\n";
let p = parse_with_shading(bytes, None, None, None);
assert_eq!(p.shadings.len(), 1);
let s = &p.shadings[0];
assert_eq!(s.name, "Sh1");
assert!(s.shading_dict.is_none());
assert!((s.ctm.e - 50.0).abs() < 1e-3);
assert!((s.ctm.f - 60.0).abs() < 1e-3);
}
#[test]
fn sh_multiple_events_surface_in_stream_order() {
let shadings = Dict::new()
.with("Sh1", Object::Dict(Dict::new()))
.with("Sh2", Object::Dict(Dict::new()));
let bytes = b"q 1 0 0 1 10 20 cm /Sh1 sh Q q 1 0 0 1 30 40 cm /Sh2 sh Q\n";
let p = parse_with_shading(bytes, None, None, Some(&shadings));
assert_eq!(p.shadings.len(), 2);
assert_eq!(p.shadings[0].name, "Sh1");
assert!((p.shadings[0].ctm.e - 10.0).abs() < 1e-3);
assert!((p.shadings[0].ctm.f - 20.0).abs() < 1e-3);
assert_eq!(p.shadings[1].name, "Sh2");
assert!((p.shadings[1].ctm.e - 30.0).abs() < 1e-3);
assert!((p.shadings[1].ctm.f - 40.0).abs() < 1e-3);
}
#[test]
fn parse_content_stream_full_still_drops_sh_with_none_dict() {
let bytes = b"q /Sh1 sh Q\n";
let p = parse_content_stream_full(bytes, None, None).unwrap();
assert_eq!(p.shadings.len(), 1);
assert_eq!(p.shadings[0].name, "Sh1");
assert!(p.shadings[0].shading_dict.is_none());
}
#[test]
fn shadings_empty_when_no_sh_operator() {
let shadings = shading_res_with("Sh1", Dict::new());
let bytes = b"q 100 100 m 200 200 l S Q\n";
let p = parse_with_shading(bytes, None, None, Some(&shadings));
assert!(p.shadings.is_empty());
}
struct BitWriter {
bytes: Vec<u8>,
bit: u32,
}
impl BitWriter {
fn new() -> Self {
Self {
bytes: Vec::new(),
bit: 0,
}
}
fn write(&mut self, value: u64, bits: u32) {
for i in (0..bits).rev() {
if self.bit == 0 {
self.bytes.push(0);
}
let b = ((value >> i) & 1) as u8;
let last = self.bytes.len() - 1;
self.bytes[last] |= b << (7 - self.bit);
self.bit = (self.bit + 1) % 8;
}
}
fn align(&mut self) {
self.bit = 0;
}
fn finish(mut self) -> Vec<u8> {
self.align();
self.bytes
}
}
fn decode_rgb8() -> Object {
Object::Array(
[0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0]
.into_iter()
.map(Object::Real)
.collect(),
)
}
#[test]
fn mesh_type4_single_triangle_decodes_vertices() {
let mut w = BitWriter::new();
let vert = |w: &mut BitWriter, flag: u64, x: u64, y: u64, r: u64, g: u64, b: u64| {
w.write(flag, 8);
w.write(x, 8);
w.write(y, 8);
w.write(r, 8);
w.write(g, 8);
w.write(b, 8);
w.align();
};
vert(&mut w, 0, 0, 0, 255, 0, 0);
vert(&mut w, 0, 255, 0, 0, 255, 0);
vert(&mut w, 0, 0, 255, 0, 0, 255);
let data = w.finish();
let dict = Dict::new()
.with("ShadingType", Object::Integer(4))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with("BitsPerCoordinate", Object::Integer(8))
.with("BitsPerComponent", Object::Integer(8))
.with("BitsPerFlag", Object::Integer(8))
.with("Decode", decode_rgb8())
.with("__MeshData", Object::HexString(data));
let mesh = evaluate_mesh_shading(&dict, None).expect("mesh evaluated");
let MeshShading::Triangles(tris) = mesh else {
panic!("expected triangles")
};
assert_eq!(tris.len(), 1);
let v = tris[0].vertices;
assert!((v[0].point.x - 0.0).abs() < 1e-4 && (v[0].point.y - 0.0).abs() < 1e-4);
assert_eq!((v[0].color.r, v[0].color.g, v[0].color.b), (255, 0, 0));
assert!((v[1].point.x - 1.0).abs() < 1e-4);
assert_eq!((v[1].color.r, v[1].color.g, v[1].color.b), (0, 255, 0));
assert!((v[2].point.y - 1.0).abs() < 1e-4);
assert_eq!((v[2].color.r, v[2].color.g, v[2].color.b), (0, 0, 255));
}
#[test]
fn mesh_type4_edge_flag_continuation() {
let mut w = BitWriter::new();
let vert = |w: &mut BitWriter, flag: u64, x: u64, y: u64, r: u64, g: u64, b: u64| {
w.write(flag, 8);
w.write(x, 8);
w.write(y, 8);
w.write(r, 8);
w.write(g, 8);
w.write(b, 8);
w.align();
};
vert(&mut w, 0, 0, 0, 255, 0, 0); vert(&mut w, 0, 255, 0, 0, 255, 0); vert(&mut w, 0, 0, 255, 0, 0, 255); vert(&mut w, 1, 255, 255, 255, 255, 0); let data = w.finish();
let dict = Dict::new()
.with("ShadingType", Object::Integer(4))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with("BitsPerCoordinate", Object::Integer(8))
.with("BitsPerComponent", Object::Integer(8))
.with("BitsPerFlag", Object::Integer(8))
.with("Decode", decode_rgb8())
.with("__MeshData", Object::HexString(data));
let MeshShading::Triangles(tris) = evaluate_mesh_shading(&dict, None).unwrap() else {
panic!()
};
assert_eq!(tris.len(), 2);
let t2 = tris[1].vertices;
assert!((t2[0].point.x - 1.0).abs() < 1e-4 && t2[0].point.y.abs() < 1e-4); assert!(t2[1].point.x.abs() < 1e-4 && (t2[1].point.y - 1.0).abs() < 1e-4); assert!((t2[2].point.x - 1.0).abs() < 1e-4 && (t2[2].point.y - 1.0).abs() < 1e-4); assert_eq!((t2[2].color.r, t2[2].color.g, t2[2].color.b), (255, 255, 0));
}
#[test]
fn mesh_type5_lattice_two_by_two() {
let mut w = BitWriter::new();
let vert = |w: &mut BitWriter, x: u64, y: u64, r: u64, g: u64, b: u64| {
w.write(x, 8);
w.write(y, 8);
w.write(r, 8);
w.write(g, 8);
w.write(b, 8);
w.align();
};
vert(&mut w, 0, 0, 255, 0, 0);
vert(&mut w, 255, 0, 0, 255, 0);
vert(&mut w, 0, 255, 0, 0, 255);
vert(&mut w, 255, 255, 255, 255, 255);
let data = w.finish();
let dict = Dict::new()
.with("ShadingType", Object::Integer(5))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with("BitsPerCoordinate", Object::Integer(8))
.with("BitsPerComponent", Object::Integer(8))
.with("VerticesPerRow", Object::Integer(2))
.with("Decode", decode_rgb8())
.with("__MeshData", Object::HexString(data));
let MeshShading::Triangles(tris) = evaluate_mesh_shading(&dict, None).unwrap() else {
panic!()
};
assert_eq!(tris.len(), 2);
let t = tris[0].vertices;
assert_eq!((t[0].color.r, t[0].color.g, t[0].color.b), (255, 0, 0));
assert_eq!((t[1].color.r, t[1].color.g, t[1].color.b), (0, 255, 0));
assert_eq!((t[2].color.r, t[2].color.g, t[2].color.b), (0, 0, 255));
}
#[test]
fn mesh_type6_coons_single_patch() {
let mut w = BitWriter::new();
w.write(0, 8); let pts: [(u64, u64); 12] = [
(0, 0), (0, 85), (0, 170), (0, 255), (85, 255), (170, 255), (255, 255), (255, 170), (255, 85), (255, 0), (170, 0), (85, 0), ];
for (x, y) in pts {
w.write(x, 8);
w.write(y, 8);
}
let cols: [(u64, u64, u64); 4] = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)];
for (r, g, b) in cols {
w.write(r, 8);
w.write(g, 8);
w.write(b, 8);
}
w.align();
let data = w.finish();
let dict = Dict::new()
.with("ShadingType", Object::Integer(6))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with("BitsPerCoordinate", Object::Integer(8))
.with("BitsPerComponent", Object::Integer(8))
.with("BitsPerFlag", Object::Integer(8))
.with("Decode", decode_rgb8())
.with("__MeshData", Object::HexString(data));
let MeshShading::Patches(patches) = evaluate_mesh_shading(&dict, None).unwrap() else {
panic!("expected patches")
};
assert_eq!(patches.len(), 1);
let p = &patches[0];
assert!((p.control_points[0][0].x).abs() < 1e-4); assert!((p.control_points[3][3].x - 1.0).abs() < 1e-4); assert_eq!(
(
p.corner_colors[0].r,
p.corner_colors[0].g,
p.corner_colors[0].b
),
(255, 0, 0)
);
assert_eq!(
(
p.corner_colors[2].r,
p.corner_colors[2].g,
p.corner_colors[2].b
),
(0, 0, 255)
);
for c in 1..=2 {
for rr in 1..=2 {
let ip = p.control_points[c][rr];
assert!(ip.x > -0.5 && ip.x < 1.5, "internal x in range");
assert!(ip.y > -0.5 && ip.y < 1.5, "internal y in range");
}
}
}
#[test]
fn mesh_type7_tensor_single_patch() {
let mut w = BitWriter::new();
w.write(0, 8); let pts: [(u64, u64); 16] = [
(0, 0), (0, 85), (0, 170), (0, 255), (85, 255), (170, 255), (255, 255), (255, 170), (255, 85), (255, 0), (170, 0), (85, 0), (85, 85), (85, 170), (170, 170), (170, 85), ];
for (x, y) in pts {
w.write(x, 8);
w.write(y, 8);
}
for (r, g, b) in [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)] {
w.write(r, 8);
w.write(g, 8);
w.write(b, 8);
}
w.align();
let data = w.finish();
let dict = Dict::new()
.with("ShadingType", Object::Integer(7))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with("BitsPerCoordinate", Object::Integer(8))
.with("BitsPerComponent", Object::Integer(8))
.with("BitsPerFlag", Object::Integer(8))
.with("Decode", decode_rgb8())
.with("__MeshData", Object::HexString(data));
let MeshShading::Patches(patches) = evaluate_mesh_shading(&dict, None).unwrap() else {
panic!()
};
assert_eq!(patches.len(), 1);
let p = &patches[0];
assert!((p.control_points[1][1].x - 85.0 / 255.0).abs() < 1e-3);
assert!((p.control_points[2][2].y - 170.0 / 255.0).abs() < 1e-3);
assert_eq!(
(
p.corner_colors[3].r,
p.corner_colors[3].g,
p.corner_colors[3].b
),
(255, 255, 0)
);
}
#[test]
fn mesh_type6_coons_edge_flag_continuation() {
let mut w = BitWriter::new();
w.write(0, 8);
let pts_a: [(u64, u64); 12] = [
(0, 0),
(0, 85),
(0, 170),
(0, 255),
(85, 255),
(170, 255),
(255, 255),
(255, 170),
(255, 85),
(255, 0),
(170, 0),
(85, 0),
];
for (x, y) in pts_a {
w.write(x, 8);
w.write(y, 8);
}
for (r, g, b) in [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)] {
w.write(r, 8);
w.write(g, 8);
w.write(b, 8);
}
w.align();
w.write(1, 8);
for k in 0..8u64 {
w.write(255, 8);
w.write((k * 30).min(255), 8);
}
for (r, g, b) in [(255, 255, 0), (255, 0, 255)] {
w.write(r, 8);
w.write(g, 8);
w.write(b, 8);
}
w.align();
let data = w.finish();
let dict = Dict::new()
.with("ShadingType", Object::Integer(6))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with("BitsPerCoordinate", Object::Integer(8))
.with("BitsPerComponent", Object::Integer(8))
.with("BitsPerFlag", Object::Integer(8))
.with("Decode", decode_rgb8())
.with("__MeshData", Object::HexString(data));
let MeshShading::Patches(patches) = evaluate_mesh_shading(&dict, None).unwrap() else {
panic!()
};
assert_eq!(patches.len(), 2);
let b = &patches[1];
let a = &patches[0];
assert_eq!(b.control_points[0][0], a.control_points[0][3]); assert_eq!(b.control_points[0][3], a.control_points[3][3]); assert_eq!(
(
b.corner_colors[0].r,
b.corner_colors[0].g,
b.corner_colors[0].b
),
(0, 255, 0)
);
assert_eq!(
(
b.corner_colors[1].r,
b.corner_colors[1].g,
b.corner_colors[1].b
),
(0, 0, 255)
);
assert_eq!(
(
b.corner_colors[2].r,
b.corner_colors[2].g,
b.corner_colors[2].b
),
(255, 255, 0)
);
assert_eq!(
(
b.corner_colors[3].r,
b.corner_colors[3].g,
b.corner_colors[3].b
),
(255, 0, 255)
);
}
#[test]
fn mesh_type4_with_parametric_function() {
let mut w = BitWriter::new();
let vert = |w: &mut BitWriter, x: u64, y: u64, t: u64| {
w.write(0, 8); w.write(x, 8);
w.write(y, 8);
w.write(t, 8);
w.align();
};
vert(&mut w, 0, 0, 0); vert(&mut w, 255, 0, 255); vert(&mut w, 0, 255, 128); let data = w.finish();
let func = Dict::new()
.with("FunctionType", Object::Integer(2))
.with(
"Domain",
Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
)
.with(
"C0",
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(0.0),
]),
)
.with(
"C1",
Object::Array(vec![
Object::Real(1.0),
Object::Real(1.0),
Object::Real(1.0),
]),
)
.with("N", Object::Real(1.0));
let decode = Object::Array(
[0.0, 1.0, 0.0, 1.0, 0.0, 1.0]
.into_iter()
.map(Object::Real)
.collect(),
);
let dict = Dict::new()
.with("ShadingType", Object::Integer(4))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with("BitsPerCoordinate", Object::Integer(8))
.with("BitsPerComponent", Object::Integer(8))
.with("BitsPerFlag", Object::Integer(8))
.with("Decode", decode)
.with("Function", Object::Dict(func))
.with("__MeshData", Object::HexString(data));
let MeshShading::Triangles(tris) = evaluate_mesh_shading(&dict, None).unwrap() else {
panic!()
};
let v = tris[0].vertices;
assert_eq!((v[0].color.r, v[0].color.g, v[0].color.b), (0, 0, 0));
assert_eq!((v[1].color.r, v[1].color.g, v[1].color.b), (255, 255, 255));
assert!((v[2].color.r as i32 - 128).abs() <= 1);
assert_eq!(v[2].color.r, v[2].color.g);
assert_eq!(v[2].color.g, v[2].color.b);
}
#[test]
fn mesh_none_for_axial_shading() {
let dict = Dict::new()
.with("ShadingType", Object::Integer(2))
.with("ColorSpace", Object::Name("DeviceRGB".into()));
assert!(evaluate_mesh_shading(&dict, None).is_none());
}
#[test]
fn sh_surfaces_evaluated_mesh() {
let mut w = BitWriter::new();
let vert = |w: &mut BitWriter, x: u64, y: u64, r: u64, g: u64, b: u64| {
w.write(0, 8);
w.write(x, 8);
w.write(y, 8);
w.write(r, 8);
w.write(g, 8);
w.write(b, 8);
w.align();
};
vert(&mut w, 0, 0, 255, 0, 0);
vert(&mut w, 255, 0, 0, 255, 0);
vert(&mut w, 0, 255, 0, 0, 255);
let data = w.finish();
let sh1 = Dict::new()
.with("ShadingType", Object::Integer(4))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with("BitsPerCoordinate", Object::Integer(8))
.with("BitsPerComponent", Object::Integer(8))
.with("BitsPerFlag", Object::Integer(8))
.with("Decode", decode_rgb8())
.with("__MeshData", Object::HexString(data));
let shadings = shading_res_with("Sh1", sh1);
let bytes = b"q /Sh1 sh Q\n";
let p = parse_with_shading(bytes, None, None, Some(&shadings));
assert_eq!(p.shadings.len(), 1);
let mesh = p.shadings[0].mesh.as_ref().expect("mesh surfaced");
let MeshShading::Triangles(tris) = mesh else {
panic!()
};
assert_eq!(tris.len(), 1);
}
fn exp_black_to_white() -> Object {
Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(2))
.with(
"Domain",
Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
)
.with(
"C0",
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(0.0),
]),
)
.with(
"C1",
Object::Array(vec![
Object::Real(1.0),
Object::Real(1.0),
Object::Real(1.0),
]),
)
.with("N", Object::Real(1.0)),
)
}
#[test]
fn gradient_type2_axial_samples_stops() {
let dict = Dict::new()
.with("ShadingType", Object::Integer(2))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with(
"Coords",
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(100.0),
Object::Real(0.0),
]),
)
.with("Function", exp_black_to_white())
.with(
"Extend",
Object::Array(vec![Object::Bool(true), Object::Bool(false)]),
);
let g = evaluate_gradient_shading(&dict, None).expect("gradient");
let ShadingGradient::Axial {
coords,
extend,
stops,
} = g
else {
panic!("expected axial")
};
assert_eq!(coords, [0.0, 0.0, 100.0, 0.0]);
assert_eq!(extend, [true, false]);
assert_eq!(stops.len(), 64);
assert_eq!((stops[0].r, stops[0].g, stops[0].b), (0, 0, 0));
assert_eq!((stops[63].r, stops[63].g, stops[63].b), (255, 255, 255));
assert!(stops[32].r > stops[0].r && stops[32].r < stops[63].r);
}
#[test]
fn gradient_type3_radial_samples_stops() {
let dict = Dict::new()
.with("ShadingType", Object::Integer(3))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with(
"Coords",
Object::Array(
[0.0, 0.0, 0.0, 0.0, 0.0, 50.0]
.into_iter()
.map(Object::Real)
.collect(),
),
)
.with("Function", exp_black_to_white());
let g = evaluate_gradient_shading(&dict, None).expect("gradient");
let ShadingGradient::Radial {
coords,
extend,
stops,
} = g
else {
panic!("expected radial")
};
assert_eq!(coords, [0.0, 0.0, 0.0, 0.0, 0.0, 50.0]);
assert_eq!(extend, [false, false]); assert_eq!(stops.len(), 64);
assert_eq!((stops[0].r, stops[0].g, stops[0].b), (0, 0, 0));
}
#[test]
fn shading_color_space_resolves_resource_key() {
assert_eq!(
shading_color_space(&Object::Name("DeviceRGB".into()), None),
ColorSpaceKind::DeviceRgb
);
let cal_rgb = Object::Array(vec![
Object::Name("CalRGB".into()),
Object::Dict(Dict::new().with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)),
]);
let res = Dict::new().with("CS0", cal_rgb);
assert!(matches!(
shading_color_space(&Object::Name("CS0".into()), Some(&res)),
ColorSpaceKind::CalRgb { .. }
));
assert_eq!(
shading_color_space(&Object::Name("CS9".into()), None),
ColorSpaceKind::Unknown
);
}
#[test]
fn gradient_named_resource_colour_space_resolves() {
let func = Object::Dict(
Dict::new()
.with("FunctionType", Object::Integer(2))
.with(
"Domain",
Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
)
.with("C0", Object::Array(vec![Object::Real(0.0)]))
.with("C1", Object::Array(vec![Object::Real(1.0)]))
.with("N", Object::Real(1.0)),
);
let dict = Dict::new()
.with("ShadingType", Object::Integer(2))
.with("ColorSpace", Object::Name("CSGray".into()))
.with(
"Coords",
Object::Array(
[0.0, 0.0, 100.0, 0.0]
.into_iter()
.map(Object::Real)
.collect(),
),
)
.with("Function", func);
let cal_gray = Object::Array(vec![
Object::Name("CalGray".into()),
Object::Dict(Dict::new().with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)),
]);
let res = Dict::new().with("CSGray", cal_gray);
assert!(evaluate_gradient_shading(&dict, None).is_none());
let g = evaluate_gradient_shading(&dict, Some(&res)).expect("gradient");
let ShadingGradient::Axial { stops, .. } = g else {
panic!("expected axial");
};
assert_eq!(stops.len(), 64);
assert_eq!((stops[0].r, stops[0].g, stops[0].b), (0, 0, 0));
assert_eq!((stops[63].r, stops[63].g, stops[63].b), (255, 255, 255));
}
#[test]
fn gradient_type1_function_based_grid() {
let program = b"{ pop pop 0.5 0.5 0.5 }".to_vec();
let func = Dict::new()
.with("FunctionType", Object::Integer(4))
.with(
"Domain",
Object::Array(vec![
Object::Real(0.0),
Object::Real(1.0),
Object::Real(0.0),
Object::Real(1.0),
]),
)
.with(
"Range",
Object::Array(vec![
Object::Real(0.0),
Object::Real(1.0),
Object::Real(0.0),
Object::Real(1.0),
Object::Real(0.0),
Object::Real(1.0),
]),
)
.with("__Program", Object::HexString(program));
let dict = Dict::new()
.with("ShadingType", Object::Integer(1))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with("Function", Object::Dict(func));
let g = evaluate_gradient_shading(&dict, None).expect("gradient");
let ShadingGradient::FunctionBased {
domain,
grid,
samples,
..
} = g
else {
panic!("expected function-based")
};
assert_eq!(domain, [0.0, 1.0, 0.0, 1.0]); assert_eq!(grid, (16, 16));
assert_eq!(samples.len(), 256);
for s in &samples {
assert!((s.r as i32 - 128).abs() <= 1);
assert_eq!(s.r, s.g);
assert_eq!(s.g, s.b);
}
}
#[test]
fn gradient_and_mesh_are_exclusive() {
let axial = Dict::new()
.with("ShadingType", Object::Integer(2))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with(
"Coords",
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(1.0),
Object::Real(0.0),
]),
)
.with("Function", exp_black_to_white());
assert!(evaluate_mesh_shading(&axial, None).is_none());
assert!(evaluate_gradient_shading(&axial, None).is_some());
}
#[test]
fn sh_surfaces_evaluated_gradient() {
let sh1 = Dict::new()
.with("ShadingType", Object::Integer(2))
.with("ColorSpace", Object::Name("DeviceRGB".into()))
.with(
"Coords",
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(72.0),
Object::Real(0.0),
]),
)
.with("Function", exp_black_to_white());
let shadings = shading_res_with("Sh1", sh1);
let bytes = b"q /Sh1 sh Q\n";
let p = parse_with_shading(bytes, None, None, Some(&shadings));
assert_eq!(p.shadings.len(), 1);
assert!(p.shadings[0].mesh.is_none());
let g = p.shadings[0].gradient.as_ref().expect("gradient surfaced");
assert!(matches!(g, ShadingGradient::Axial { .. }));
}
fn simple_font_with_widths(first_char: i64, widths: &[i64]) -> Dict {
let arr: Vec<Object> = widths.iter().map(|w| Object::Integer(*w)).collect();
Dict::new()
.with("Type", Object::Name("Font".into()))
.with("Subtype", Object::Name("Type1".into()))
.with("FirstChar", Object::Integer(first_char))
.with("Widths", Object::Array(arr))
}
#[test]
fn consecutive_tj_advances_text_matrix_by_widths() {
let f1 = simple_font_with_widths(65, &[500, 250]);
let fonts = font_res_with("F1", f1);
let bytes = b"BT /F1 10 Tf 0 700 Td (A) Tj (B) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 2);
assert!((p.text_shows[0].position.0 - 0.0).abs() < 1e-3);
assert!(
(p.text_shows[1].position.0 - 5.0).abs() < 1e-3,
"got {}",
p.text_shows[1].position.0
);
assert!((p.text_shows[1].position.1 - 700.0).abs() < 1e-3);
}
#[test]
fn type3_font_advances_via_font_matrix() {
let f1 = Dict::new()
.with("Type", Object::Name("Font".into()))
.with("Subtype", Object::Name("Type3".into()))
.with("FirstChar", Object::Integer(65))
.with("Widths", Object::Array(vec![Object::Integer(50)]))
.with(
"FontMatrix",
Object::Array(vec![
Object::Real(0.01),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(0.01),
Object::Real(0.0),
Object::Real(0.0),
]),
);
let fonts = font_res_with("F1", f1);
let bytes = b"BT /F1 10 Tf 0 700 Td (A) Tj (A) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 2);
assert!(
(p.text_shows[1].position.0 - 5.0).abs() < 1e-3,
"got {}",
p.text_shows[1].position.0
);
}
#[test]
fn type3_default_font_matrix_matches_type1() {
let f1 = Dict::new()
.with("Type", Object::Name("Font".into()))
.with("Subtype", Object::Name("Type3".into()))
.with("FirstChar", Object::Integer(65))
.with("Widths", Object::Array(vec![Object::Integer(500)]))
.with(
"FontMatrix",
Object::Array(vec![
Object::Real(0.001),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(0.001),
Object::Real(0.0),
Object::Real(0.0),
]),
);
let fonts = font_res_with("F1", f1);
let bytes = b"BT /F1 10 Tf 0 700 Td (A) Tj (A) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert!((p.text_shows[1].position.0 - 5.0).abs() < 1e-3);
}
#[test]
fn tc_tw_feed_the_advance() {
let mut widths = vec![0i64; 66 - 32];
widths[0] = 250; widths[65 - 32] = 500; let f1 = simple_font_with_widths(32, &widths);
let fonts = font_res_with("F1", f1);
let bytes = b"BT /F1 10 Tf 2 Tc 3 Tw 0 0 Td (A A) Tj (X) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 2);
assert!(
(p.text_shows[1].position.0 - 21.5).abs() < 1e-3,
"got {}",
p.text_shows[1].position.0
);
}
#[test]
fn tz_scales_the_advance() {
let f1 = simple_font_with_widths(65, &[1000]);
let fonts = font_res_with("F1", f1);
let bytes = b"BT /F1 10 Tf 50 Tz 0 0 Td (A) Tj (A) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 2);
assert!(
(p.text_shows[1].position.0 - 5.0).abs() < 1e-3,
"got {}",
p.text_shows[1].position.0
);
}
#[test]
fn tj_array_kern_adjusts_origin() {
let f1 = simple_font_with_widths(65, &[1000, 1000]); let fonts = font_res_with("F1", f1);
let bytes = b"BT /F1 10 Tf 0 0 Td [(A) -100 (B)] TJ (C) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 2);
assert!(
(p.text_shows[1].position.0 - 21.0).abs() < 1e-3,
"got {}",
p.text_shows[1].position.0
);
}
#[test]
fn type0_cid_font_advances_by_w_array() {
let cidfont = Dict::new()
.with("Type", Object::Name("Font".into()))
.with("Subtype", Object::Name("CIDFontType2".into()))
.with("DW", Object::Integer(1000))
.with(
"W",
Object::Array(vec![
Object::Integer(1),
Object::Array(vec![Object::Integer(500)]),
]),
);
let f0 = Dict::new()
.with("Type", Object::Name("Font".into()))
.with("Subtype", Object::Name("Type0".into()))
.with("Encoding", Object::Name("Identity-H".into()))
.with("DescendantFonts", Object::Dict(cidfont));
let fonts = font_res_with("F0", f0);
let bytes = b"BT /F0 10 Tf 0 0 Td <00010002> Tj (X) Tj ET\n";
let p = parse_full(bytes, None, Some(&fonts));
assert_eq!(p.text_shows.len(), 2);
assert!(
(p.text_shows[1].position.0 - 15.0).abs() < 1e-3,
"got {}",
p.text_shows[1].position.0
);
}
const D65: [f32; 3] = [0.9505, 1.0000, 1.0890];
#[test]
fn srgb_encode_reference_points() {
assert!((srgb_encode(0.0) - 0.0).abs() < 1e-6);
assert!((srgb_encode(1.0) - 1.0).abs() < 1e-6);
let bp = 0.003_130_8;
let lin = 12.92 * bp;
assert!((srgb_encode(bp) - lin).abs() < 1e-4);
assert!((srgb_encode(0.5) - 0.735_36).abs() < 1e-3);
}
#[test]
fn cal_gray_endpoints() {
let white = cal_gray_color(D65, 1.0, 1.0);
assert_eq!((white.r, white.g, white.b), (255, 255, 255));
let black = cal_gray_color(D65, 1.0, 0.0);
assert_eq!((black.r, black.g, black.b), (0, 0, 0));
}
#[test]
fn cal_gray_gamma_darkens_midtones() {
let g1 = cal_gray_color(D65, 1.0, 0.5).r;
let g22 = cal_gray_color(D65, 2.2, 0.5).r;
assert!(g22 < g1, "gamma 2.2 ({g22}) should darken vs 1.0 ({g1})");
}
#[test]
fn cal_rgb_example_endpoints() {
let matrix = [
0.4497, 0.2446, 0.0252, 0.3163, 0.6720, 0.1412, 0.1845, 0.0833, 0.9227,
];
let gamma = [1.8, 1.8, 1.8];
let black = cal_rgb_color(gamma, matrix, [0.0, 0.0, 0.0]);
assert_eq!((black.r, black.g, black.b), (0, 0, 0));
let white = cal_rgb_color(gamma, matrix, [1.0, 1.0, 1.0]);
assert!(white.r > 230 && white.g > 230 && white.b > 230);
let red = cal_rgb_color(gamma, matrix, [1.0, 0.0, 0.0]);
assert!(red.r > red.g && red.r > red.b);
}
#[test]
fn lab_g_breakpoint_continuous() {
let bp = 6.0 / 29.0;
let cube = bp * bp * bp;
assert!((lab_g(bp) - cube).abs() < 1e-6);
assert!((lab_g(0.5) - 0.125).abs() < 1e-6);
}
#[test]
fn lab_neutral_axis() {
let white = lab_color(D65, [100.0, 0.0, 0.0]);
assert_eq!((white.r, white.g, white.b), (255, 255, 255));
let black = lab_color(D65, [0.0, 0.0, 0.0]);
assert_eq!((black.r, black.g, black.b), (0, 0, 0));
let grey = lab_color(D65, [50.0, 0.0, 0.0]);
assert!(grey.r.abs_diff(grey.g) <= 2 && grey.g.abs_diff(grey.b) <= 2);
}
#[test]
fn lab_chroma_axes_direction() {
let reddish = lab_color(D65, [60.0, 60.0, 0.0]);
assert!(reddish.r > reddish.g, "+a* should be red-dominant");
let yellowish = lab_color(D65, [80.0, 0.0, 70.0]);
assert!(
yellowish.r > yellowish.b && yellowish.g > yellowish.b,
"+b* should be yellow (low blue)"
);
}
#[test]
fn cal_gray_resolves_from_array() {
let arr = Object::Array(vec![
Object::Name("CalGray".into()),
Object::Dict(
Dict::new()
.with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)
.with("Gamma", Object::Real(2.222)),
),
]);
match color_space_from_object(&arr) {
ColorSpaceKind::CalGray { white, gamma } => {
assert!((white[1] - 1.0).abs() < 1e-6);
assert!((gamma - 2.222).abs() < 1e-6);
}
other => panic!("expected CalGray, got {other:?}"),
}
let bad = Object::Array(vec![
Object::Name("CalGray".into()),
Object::Dict(Dict::new().with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.95),
Object::Real(0.5),
Object::Real(1.0),
]),
)),
]);
assert_eq!(color_space_from_object(&bad), ColorSpaceKind::Unknown);
}
#[test]
fn cal_gray_end_to_end_white() {
let arr = Object::Array(vec![
Object::Name("CalGray".into()),
Object::Dict(Dict::new().with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)),
]);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 sc 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 255, 255));
}
#[test]
fn lab_end_to_end_white() {
let arr = Object::Array(vec![
Object::Name("Lab".into()),
Object::Dict(
Dict::new()
.with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)
.with(
"Range",
Object::Array(vec![
Object::Integer(-128),
Object::Integer(127),
Object::Integer(-128),
Object::Integer(127),
]),
),
),
]);
match color_space_from_object(&arr) {
ColorSpaceKind::Lab { range, .. } => {
assert_eq!(range, [-128.0, 127.0, -128.0, 127.0]);
}
other => panic!("expected Lab, got {other:?}"),
}
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 100 0 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 255, 255));
}
#[test]
fn cal_rgb_resolves_default_matrix() {
let arr = Object::Array(vec![
Object::Name("CalRGB".into()),
Object::Dict(Dict::new().with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)),
]);
match color_space_from_object(&arr) {
ColorSpaceKind::CalRgb { gamma, matrix } => {
assert_eq!(gamma, [1.0, 1.0, 1.0]);
assert_eq!(matrix, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
}
other => panic!("expected CalRgb, got {other:?}"),
}
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 1 1 scn 0 0 m 10 10 l 10 0 l h f Q\n";
let (r, g, b) = first_fill_with_cs(bytes, &cs);
assert!(r > 230 && g > 230 && b > 230);
}
fn cal_gray_obj() -> Object {
Object::Array(vec![
Object::Name("CalGray".into()),
Object::Dict(Dict::new().with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)),
])
}
fn lab_obj() -> Object {
Object::Array(vec![
Object::Name("Lab".into()),
Object::Dict(Dict::new().with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)),
])
}
#[test]
fn separation_calgray_alternate_renders() {
let tint = type2(&[0.0], &[1.0], 1.0);
let arr = separation("Spot", cal_gray_obj(), tint);
match color_space_from_object(&arr) {
ColorSpaceKind::Separation { alt, .. } => {
assert!(matches!(*alt, ColorSpaceKind::CalGray { .. }));
}
other => panic!("expected Separation/CalGray, got {other:?}"),
}
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 255, 255));
let bytes = b"q /CS0 cs 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 0, 0));
}
#[test]
fn device_n_lab_alternate_renders() {
let tint = type4(
&[0.0, 1.0, 0.0, 1.0],
&[0.0, 100.0, -128.0, 127.0, -128.0, 127.0],
"{ pop 100 mul 0 0 }",
);
let arr = device_n(&["C0", "C1"], lab_obj(), tint);
match color_space_from_object(&arr) {
ColorSpaceKind::DeviceN { alt, n_in, .. } => {
assert_eq!(n_in, 2);
assert!(matches!(*alt, ColorSpaceKind::Lab { .. }));
}
other => panic!("expected DeviceN/Lab, got {other:?}"),
}
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 1 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (255, 255, 255));
let bytes = b"q /CS0 cs 0 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 0, 0));
}
#[test]
fn device_n_cie_alternate_arity_mismatch_rejected() {
let cal_rgb = Object::Array(vec![
Object::Name("CalRGB".into()),
Object::Dict(Dict::new().with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)),
]);
let tint = type2(&[0.0, 0.0], &[1.0, 1.0], 1.0);
let arr = device_n(&["C0"], cal_rgb, tint);
assert_eq!(color_space_from_object(&arr), ColorSpaceKind::Unknown);
}
#[test]
fn indexed_calrgb_base_renders() {
let cal_rgb = Object::Array(vec![
Object::Name("CalRGB".into()),
Object::Dict(Dict::new().with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)),
]);
let table = Object::HexString(vec![255, 255, 255, 0, 0, 0]);
let arr = Object::Array(vec![
Object::Name("Indexed".into()),
cal_rgb,
Object::Integer(1),
table,
]);
match color_space_from_object(&arr) {
ColorSpaceKind::Indexed { base, hival, .. } => {
assert_eq!(hival, 1);
assert!(matches!(*base, ColorSpaceKind::CalRgb { .. }));
}
other => panic!("expected Indexed/CalRGB, got {other:?}"),
}
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
let (r, g, b) = first_fill_with_cs(bytes, &cs);
assert!(r > 230 && g > 230 && b > 230, "entry 0 got ({r},{g},{b})");
let bytes = b"q /CS0 cs 1 scn 0 0 m 10 10 l 10 0 l h f Q\n";
assert_eq!(first_fill_with_cs(bytes, &cs), (0, 0, 0));
}
#[test]
fn indexed_lab_base_decodes_table() {
let lab = Object::Array(vec![
Object::Name("Lab".into()),
Object::Dict(
Dict::new()
.with(
"WhitePoint",
Object::Array(vec![
Object::Real(0.9505),
Object::Real(1.0),
Object::Real(1.089),
]),
)
.with(
"Range",
Object::Array(vec![
Object::Integer(-128),
Object::Integer(127),
Object::Integer(-128),
Object::Integer(127),
]),
),
),
]);
let table = Object::HexString(vec![255, 128, 128]);
let arr = Object::Array(vec![
Object::Name("Indexed".into()),
lab,
Object::Integer(0),
table,
]);
let cs = Dict::new().with("CS0", arr);
let bytes = b"q /CS0 cs 0 scn 0 0 m 10 10 l 10 0 l h f Q\n";
let (r, g, b) = first_fill_with_cs(bytes, &cs);
assert!(r > 230 && g > 230 && b > 230, "got ({r},{g},{b})");
}
}