mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 04:26:55 -06:00
587 lines
25 KiB
Rust
587 lines
25 KiB
Rust
//! Colour spaces (ISO 32000-1 §8.6) reduced to sRGB, and functions (§7.10: sampled,
|
|
//! exponential, stitching and PostScript calculator functions).
|
|
|
|
use crate::object::{Dict, File, Obj, decode_stream};
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum Cs {
|
|
Gray,
|
|
Rgb,
|
|
Cmyk,
|
|
/// CIE L*a*b* (approximated).
|
|
Lab,
|
|
Indexed {
|
|
base: Box<Cs>,
|
|
hival: usize,
|
|
lookup: Vec<u8>,
|
|
},
|
|
/// Separation / DeviceN: components → alternate space through a tint transform.
|
|
Tint {
|
|
n: usize,
|
|
alt: Box<Cs>,
|
|
func: Func,
|
|
},
|
|
Pattern,
|
|
}
|
|
|
|
impl Cs {
|
|
pub fn components(&self) -> usize {
|
|
match self {
|
|
Cs::Gray | Cs::Indexed { .. } => 1,
|
|
Cs::Rgb | Cs::Lab => 3,
|
|
Cs::Cmyk => 4,
|
|
Cs::Tint { n, .. } => *n,
|
|
Cs::Pattern => 0,
|
|
}
|
|
}
|
|
|
|
/// The initial colour of the space (black, or tint 1).
|
|
pub fn initial(&self) -> Vec<f64> {
|
|
match self {
|
|
Cs::Cmyk => vec![0.0, 0.0, 0.0, 1.0],
|
|
Cs::Tint { n, .. } => vec![1.0; *n],
|
|
Cs::Lab => vec![0.0, 0.0, 0.0],
|
|
c => vec![0.0; c.components()],
|
|
}
|
|
}
|
|
|
|
pub fn to_rgb(&self, c: &[f64]) -> [f64; 3] {
|
|
let g = |i: usize| c.get(i).copied().unwrap_or(0.0).clamp(0.0, 1.0);
|
|
match self {
|
|
Cs::Gray => [g(0); 3],
|
|
Cs::Rgb | Cs::Pattern => [g(0), g(1), g(2)],
|
|
Cs::Cmyk => {
|
|
let k = g(3);
|
|
[(1.0 - g(0)) * (1.0 - k), (1.0 - g(1)) * (1.0 - k), (1.0 - g(2)) * (1.0 - k)]
|
|
}
|
|
Cs::Lab => lab_to_rgb(c.first().copied().unwrap_or(0.0), c.get(1).copied().unwrap_or(0.0), c.get(2).copied().unwrap_or(0.0)),
|
|
Cs::Indexed { base, hival, lookup } => {
|
|
let i = (c.first().copied().unwrap_or(0.0).round().max(0.0) as usize).min(*hival);
|
|
let n = base.components();
|
|
let v: Vec<f64> = (0..n).map(|k| lookup.get(i * n + k).copied().unwrap_or(0) as f64 / 255.0).collect();
|
|
base.to_rgb(&v)
|
|
}
|
|
Cs::Tint { alt, func, .. } => alt.to_rgb(&func.eval(c)),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn lab_to_rgb(l: f64, a: f64, b: f64) -> [f64; 3] {
|
|
let fy = (l + 16.0) / 116.0;
|
|
let fx = fy + a / 500.0;
|
|
let fz = fy - b / 200.0;
|
|
let f = |t: f64| if t > 6.0 / 29.0 { t * t * t } else { 3.0 * (6.0f64 / 29.0).powi(2) * (t - 4.0 / 29.0) };
|
|
let (x, y, z) = (0.9505 * f(fx), f(fy), 1.089 * f(fz));
|
|
let lin = [3.2406 * x - 1.5372 * y - 0.4986 * z, -0.9689 * x + 1.8758 * y + 0.0415 * z, 0.0557 * x - 0.204 * y + 1.057 * z];
|
|
lin.map(|v| {
|
|
let v = v.clamp(0.0, 1.0);
|
|
if v <= 0.003_130_8 { 12.92 * v } else { 1.055 * v.powf(1.0 / 2.4) - 0.055 }
|
|
})
|
|
}
|
|
|
|
/// Read a colour space (a name, or an array such as `[/ICCBased 5 0 R]`); named resources are
|
|
/// looked up in `res` (`/ColorSpace`).
|
|
pub fn color_space(file: &File, o: &Obj, res: Option<&Dict>) -> Cs {
|
|
let o = file.resolve(o);
|
|
match o {
|
|
Obj::Name(n) => match n.as_str() {
|
|
"DeviceGray" | "G" | "CalGray" => Cs::Gray,
|
|
"DeviceRGB" | "RGB" | "CalRGB" => Cs::Rgb,
|
|
"DeviceCMYK" | "CMYK" => Cs::Cmyk,
|
|
"Pattern" => Cs::Pattern,
|
|
other => match res.and_then(|r| file.get_dict(r, "ColorSpace")).and_then(|cs| file.get(cs, other)) {
|
|
Some(x) if !matches!(x, Obj::Name(m) if m == other) => color_space(file, x, None),
|
|
_ => Cs::Gray,
|
|
},
|
|
},
|
|
Obj::Array(a) => {
|
|
let head = a.first().map(|h| file.resolve(h)).and_then(Obj::name).unwrap_or("");
|
|
match head {
|
|
"ICCBased" => {
|
|
let d = a.get(1).map(|s| file.resolve(s)).and_then(Obj::dict);
|
|
// The profile's alternate space when it names one (profiles are not
|
|
// evaluated), else the device space with the same number of components.
|
|
if let Some(alt) = d.and_then(|d| file.get(d, "Alternate")) {
|
|
return color_space(file, alt, res);
|
|
}
|
|
let n = d.and_then(|d| file.get_num(d, "N")).unwrap_or(3.0) as usize;
|
|
match n {
|
|
1 => Cs::Gray,
|
|
4 => Cs::Cmyk,
|
|
_ => Cs::Rgb,
|
|
}
|
|
}
|
|
"CalRGB" => Cs::Rgb,
|
|
"CalGray" => Cs::Gray,
|
|
"Lab" => Cs::Lab,
|
|
"Pattern" => Cs::Pattern,
|
|
"Indexed" | "I" => {
|
|
let base = a.get(1).map(|b| color_space(file, b, res)).unwrap_or(Cs::Rgb);
|
|
let hival = a.get(2).and_then(|h| file.resolve(h).num()).unwrap_or(0.0) as usize;
|
|
let lookup = match a.get(3).map(|l| file.resolve(l)) {
|
|
Some(Obj::Str(s)) => s.clone(),
|
|
Some(Obj::Stream(d, raw)) => decode_stream(file, d, raw).unwrap_or_default(),
|
|
_ => vec![],
|
|
};
|
|
Cs::Indexed { base: Box::new(base), hival, lookup }
|
|
}
|
|
"Separation" | "DeviceN" => {
|
|
let n = if head == "Separation" { 1 } else { a.get(1).map(|x| file.resolve(x)).and_then(Obj::array).map_or(1, <[Obj]>::len) };
|
|
let alt = a.get(2).map(|b| color_space(file, b, res)).unwrap_or(Cs::Gray);
|
|
let func = a.get(3).map(|f| Func::read(file, f)).unwrap_or(Func::Unsupported);
|
|
// An unreadable tint transform: tint 1 = black, 0 = white.
|
|
if func == Func::Unsupported {
|
|
return Cs::Tint { n, alt: Box::new(Cs::Gray), func: Func::Exp { domain: [0.0, 1.0], c0: vec![1.0], c1: vec![0.0], n: 1.0 } };
|
|
}
|
|
Cs::Tint { n, alt: Box::new(alt), func }
|
|
}
|
|
_ => Cs::Rgb,
|
|
}
|
|
}
|
|
_ => Cs::Gray,
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum Func {
|
|
/// Type 2.
|
|
Exp {
|
|
domain: [f64; 2],
|
|
c0: Vec<f64>,
|
|
c1: Vec<f64>,
|
|
n: f64,
|
|
},
|
|
/// Type 3.
|
|
Stitch {
|
|
domain: [f64; 2],
|
|
funcs: Vec<Func>,
|
|
bounds: Vec<f64>,
|
|
encode: Vec<f64>,
|
|
},
|
|
/// Type 0 with one input.
|
|
Sampled {
|
|
domain: [f64; 2],
|
|
size: usize,
|
|
outputs: usize,
|
|
encode: [f64; 2],
|
|
decode: Vec<f64>,
|
|
samples: Vec<f64>,
|
|
},
|
|
/// Type 0 with several inputs (function-based shadings): multilinear interpolation, the
|
|
/// first input varying fastest in the sample table.
|
|
SampledN {
|
|
domain: Vec<f64>,
|
|
size: Vec<usize>,
|
|
outputs: usize,
|
|
encode: Vec<f64>,
|
|
decode: Vec<f64>,
|
|
samples: Vec<f64>,
|
|
},
|
|
/// An array of one-output functions.
|
|
Array(Vec<Func>),
|
|
/// Type 4: a PostScript calculator program (§7.10.5).
|
|
Calc {
|
|
domain: Vec<f64>,
|
|
range: Vec<f64>,
|
|
code: Vec<PsOp>,
|
|
},
|
|
Unsupported,
|
|
}
|
|
|
|
/// A PostScript calculator token; procedures nest for `if` / `ifelse`.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum PsOp {
|
|
Num(f64),
|
|
Op(String),
|
|
Proc(Vec<PsOp>),
|
|
}
|
|
|
|
fn ps_parse(lx: &mut crate::object::Lexer, depth: usize) -> Vec<PsOp> {
|
|
let mut out = vec![];
|
|
while let Some(o) = lx.next() {
|
|
match o {
|
|
Obj::Num(n) => out.push(PsOp::Num(n)),
|
|
Obj::Bool(b) => out.push(PsOp::Num(if b { 1.0 } else { 0.0 })),
|
|
Obj::Op(op) if op == "{" => {
|
|
if depth < 32 {
|
|
out.push(PsOp::Proc(ps_parse(lx, depth + 1)));
|
|
}
|
|
}
|
|
Obj::Op(op) if op == "}" => return out,
|
|
Obj::Op(op) => out.push(PsOp::Op(op)),
|
|
_ => {}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn ps_run(code: &[PsOp], st: &mut Vec<f64>, depth: usize) {
|
|
if depth > 32 {
|
|
return;
|
|
}
|
|
let mut procs: Vec<&[PsOp]> = vec![];
|
|
for op in code {
|
|
match op {
|
|
PsOp::Num(n) => st.push(*n),
|
|
PsOp::Proc(p) => procs.push(p),
|
|
PsOp::Op(o) => {
|
|
let pop = |st: &mut Vec<f64>| st.pop().unwrap_or(0.0);
|
|
let b = |v: bool| if v { 1.0 } else { 0.0 };
|
|
match o.as_str() {
|
|
"if" => {
|
|
let p = procs.pop();
|
|
if pop(st) != 0.0
|
|
&& let Some(p) = p
|
|
{
|
|
ps_run(p, st, depth + 1);
|
|
}
|
|
}
|
|
"ifelse" => {
|
|
let (f, t) = (procs.pop(), procs.pop());
|
|
let c = pop(st) != 0.0;
|
|
if let Some(p) = if c { t } else { f } {
|
|
ps_run(p, st, depth + 1);
|
|
}
|
|
}
|
|
"true" => st.push(1.0),
|
|
"false" => st.push(0.0),
|
|
"pop" => {
|
|
st.pop();
|
|
}
|
|
"dup" => {
|
|
let v = st.last().copied().unwrap_or(0.0);
|
|
st.push(v);
|
|
}
|
|
"exch" => {
|
|
let n = st.len();
|
|
if n >= 2 {
|
|
st.swap(n - 1, n - 2);
|
|
}
|
|
}
|
|
"copy" => {
|
|
let k = pop(st).max(0.0) as usize;
|
|
let n = st.len();
|
|
if k <= n {
|
|
let tail = st[n - k..].to_vec();
|
|
st.extend(tail);
|
|
}
|
|
}
|
|
"index" => {
|
|
let k = pop(st).max(0.0) as usize;
|
|
let v = st.len().checked_sub(k + 1).map(|i| st[i]).unwrap_or(0.0);
|
|
st.push(v);
|
|
}
|
|
"roll" => {
|
|
let j = pop(st) as i64;
|
|
let n = pop(st).max(0.0) as usize;
|
|
let len = st.len();
|
|
if n > 0 && n <= len {
|
|
let s = &mut st[len - n..];
|
|
let r = j.rem_euclid(n as i64) as usize;
|
|
s.rotate_right(r);
|
|
}
|
|
}
|
|
_ => {
|
|
// Unary and binary operators.
|
|
let unary: Option<fn(f64) -> f64> = match o.as_str() {
|
|
"abs" => Some(f64::abs),
|
|
"neg" => Some(|x| -x),
|
|
"ceiling" => Some(f64::ceil),
|
|
"floor" => Some(f64::floor),
|
|
"round" => Some(|x: f64| (x + 0.5).floor()),
|
|
"truncate" | "cvi" => Some(f64::trunc),
|
|
"cvr" => Some(|x| x),
|
|
"sqrt" => Some(|x: f64| x.max(0.0).sqrt()),
|
|
"sin" => Some(|x: f64| x.to_radians().sin()),
|
|
"cos" => Some(|x: f64| x.to_radians().cos()),
|
|
"ln" => Some(|x: f64| x.max(1e-300).ln()),
|
|
"log" => Some(|x: f64| x.max(1e-300).log10()),
|
|
"not" => Some(|x: f64| {
|
|
if x == 0.0 {
|
|
1.0
|
|
} else if x == 1.0 {
|
|
0.0
|
|
} else {
|
|
!(x as i64) as f64
|
|
}
|
|
}),
|
|
_ => None,
|
|
};
|
|
if let Some(f) = unary {
|
|
let x = pop(st);
|
|
st.push(f(x));
|
|
continue;
|
|
}
|
|
let y = pop(st);
|
|
let x = pop(st);
|
|
let v = match o.as_str() {
|
|
"add" => x + y,
|
|
"sub" => x - y,
|
|
"mul" => x * y,
|
|
"div" => {
|
|
if y != 0.0 {
|
|
x / y
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
"idiv" => {
|
|
if y as i64 != 0 {
|
|
((x as i64) / (y as i64)) as f64
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
"mod" => {
|
|
if y as i64 != 0 {
|
|
((x as i64) % (y as i64)) as f64
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
"exp" => x.powf(y),
|
|
"atan" => {
|
|
let a = x.atan2(y).to_degrees();
|
|
if a < 0.0 { a + 360.0 } else { a }
|
|
}
|
|
"eq" => b(x == y),
|
|
"ne" => b(x != y),
|
|
"gt" => b(x > y),
|
|
"ge" => b(x >= y),
|
|
"lt" => b(x < y),
|
|
"le" => b(x <= y),
|
|
"and" => ((x as i64) & (y as i64)) as f64,
|
|
"or" => ((x as i64) | (y as i64)) as f64,
|
|
"xor" => ((x as i64) ^ (y as i64)) as f64,
|
|
"bitshift" => {
|
|
let (v, s) = (x as i64, y as i64);
|
|
(if s >= 0 { v << s.min(62) } else { v >> (-s).min(62) }) as f64
|
|
}
|
|
_ => {
|
|
st.push(x);
|
|
y
|
|
}
|
|
};
|
|
st.push(v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Func {
|
|
pub fn read(file: &File, o: &Obj) -> Func {
|
|
let o = file.resolve(o);
|
|
if let Obj::Array(a) = o {
|
|
return Func::Array(a.iter().map(|f| Func::read(file, f)).collect());
|
|
}
|
|
let Some(d) = o.dict() else { return Func::Unsupported };
|
|
let dom = file.get(d, "Domain").map(|x| file.nums(x)).unwrap_or_default();
|
|
let domain = [dom.first().copied().unwrap_or(0.0), dom.get(1).copied().unwrap_or(1.0)];
|
|
match file.get_num(d, "FunctionType").unwrap_or(-1.0) as i32 {
|
|
2 => Func::Exp {
|
|
domain,
|
|
c0: file.get(d, "C0").map(|x| file.nums(x)).unwrap_or_else(|| vec![0.0]),
|
|
c1: file.get(d, "C1").map(|x| file.nums(x)).unwrap_or_else(|| vec![1.0]),
|
|
n: file.get_num(d, "N").unwrap_or(1.0),
|
|
},
|
|
3 => Func::Stitch {
|
|
domain,
|
|
funcs: file.get(d, "Functions").and_then(Obj::array).map(|a| a.iter().map(|f| Func::read(file, f)).collect()).unwrap_or_default(),
|
|
bounds: file.get(d, "Bounds").map(|x| file.nums(x)).unwrap_or_default(),
|
|
encode: file.get(d, "Encode").map(|x| file.nums(x)).unwrap_or_default(),
|
|
},
|
|
0 => {
|
|
let Obj::Stream(sd, raw) = o else { return Func::Unsupported };
|
|
let Some(data) = decode_stream(file, sd, raw) else { return Func::Unsupported };
|
|
let sizes: Vec<usize> = file.get(d, "Size").map(|x| file.nums(x)).unwrap_or_default().iter().map(|v| v.max(0.0) as usize).collect();
|
|
let size = sizes.first().copied().unwrap_or(0);
|
|
let bps = file.get_num(d, "BitsPerSample").unwrap_or(8.0) as usize;
|
|
let range = file.get(d, "Range").map(|x| file.nums(x)).unwrap_or_default();
|
|
let outputs = range.len() / 2;
|
|
if size == 0 || outputs == 0 || !matches!(bps, 1 | 2 | 4 | 8 | 12 | 16 | 24 | 32) {
|
|
return Func::Unsupported;
|
|
}
|
|
let total = sizes.iter().try_fold(1usize, |a, b| a.checked_mul(*b)).filter(|t| *t > 0 && *t <= 1 << 22);
|
|
let Some(total) = total else { return Func::Unsupported };
|
|
let enc = file.get(d, "Encode").map(|x| file.nums(x)).unwrap_or_default();
|
|
let encode = [enc.first().copied().unwrap_or(0.0), enc.get(1).copied().unwrap_or(size as f64 - 1.0)];
|
|
let decode = file.get(d, "Decode").map(|x| file.nums(x)).filter(|v| v.len() >= outputs * 2).unwrap_or_else(|| range.clone());
|
|
let max = ((1u64 << bps) - 1) as f64;
|
|
let mut samples = Vec::with_capacity(size * outputs);
|
|
let mut bit = 0usize;
|
|
for _ in 0..total * outputs {
|
|
let mut v = 0u64;
|
|
for _ in 0..bps {
|
|
let byte = data.get(bit / 8).copied().unwrap_or(0);
|
|
v = (v << 1) | ((byte >> (7 - bit % 8)) & 1) as u64;
|
|
bit += 1;
|
|
}
|
|
samples.push(v as f64 / max);
|
|
}
|
|
if sizes.len() > 1 {
|
|
let m = sizes.len();
|
|
let encode =
|
|
(0..m).flat_map(|i| [enc.get(2 * i).copied().unwrap_or(0.0), enc.get(2 * i + 1).copied().unwrap_or(sizes[i] as f64 - 1.0)]).collect();
|
|
let domain = (0..m).flat_map(|i| [dom.get(2 * i).copied().unwrap_or(0.0), dom.get(2 * i + 1).copied().unwrap_or(1.0)]).collect();
|
|
return Func::SampledN { domain, size: sizes, outputs, encode, decode, samples };
|
|
}
|
|
Func::Sampled { domain, size, outputs, encode, decode, samples }
|
|
}
|
|
4 => {
|
|
let Obj::Stream(sd, raw) = o else { return Func::Unsupported };
|
|
let Some(data) = decode_stream(file, sd, raw) else { return Func::Unsupported };
|
|
let mut lx = crate::object::Lexer::new(&data, 0);
|
|
// The program is one procedure: `{ … }`.
|
|
let code = match ps_parse(&mut lx, 0).into_iter().next() {
|
|
Some(PsOp::Proc(p)) => p,
|
|
_ => return Func::Unsupported,
|
|
};
|
|
Func::Calc { domain: dom, range: file.get(d, "Range").map(|x| file.nums(x)).unwrap_or_default(), code }
|
|
}
|
|
_ => Func::Unsupported,
|
|
}
|
|
}
|
|
|
|
/// Evaluate at the first input (the functions used by shadings and tints take one input
|
|
/// in practice; extra inputs of DeviceN tints are averaged in).
|
|
pub fn eval(&self, input: &[f64]) -> Vec<f64> {
|
|
if let Func::Calc { domain, range, code } = self {
|
|
let mut st: Vec<f64> = input
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, v)| match (domain.get(2 * i), domain.get(2 * i + 1)) {
|
|
(Some(a), Some(b)) => v.clamp(a.min(*b), a.max(*b)),
|
|
_ => *v,
|
|
})
|
|
.collect();
|
|
ps_run(code, &mut st, 0);
|
|
let n = range.len() / 2;
|
|
let k = st.len().saturating_sub(n);
|
|
return st[k..].iter().enumerate().map(|(i, v)| v.clamp(range[2 * i], range[2 * i + 1])).collect();
|
|
}
|
|
match self {
|
|
Func::SampledN { domain, size, outputs, encode, decode, samples } => {
|
|
let m = size.len();
|
|
// Each input → a sample-table coordinate, then the 2^m surrounding samples.
|
|
let mut base = vec![0usize; m];
|
|
let mut frac = vec![0.0f64; m];
|
|
for i in 0..m {
|
|
let x = input.get(i).copied().unwrap_or(0.0);
|
|
let (d0, d1) = (domain[2 * i], domain[2 * i + 1]);
|
|
let x = x.clamp(d0.min(d1), d0.max(d1));
|
|
let e = if d1 != d0 { encode[2 * i] + (x - d0) / (d1 - d0) * (encode[2 * i + 1] - encode[2 * i]) } else { encode[2 * i] };
|
|
let e = if e.is_finite() { e.clamp(0.0, size[i] as f64 - 1.0) } else { 0.0 };
|
|
base[i] = e.floor() as usize;
|
|
frac[i] = e - e.floor();
|
|
}
|
|
let corners = if m <= 6 { 1usize << m } else { 1 };
|
|
let mut out = vec![0.0; *outputs];
|
|
for c in 0..corners {
|
|
let mut w = 1.0;
|
|
let mut idx = 0usize;
|
|
let mut stride = 1usize;
|
|
for i in 0..m {
|
|
let up = m <= 6 && (c >> i) & 1 == 1;
|
|
w *= if up { frac[i] } else { 1.0 - frac[i] };
|
|
let k = if up { (base[i] + 1).min(size[i] - 1) } else { base[i] };
|
|
idx += k * stride;
|
|
stride *= size[i];
|
|
}
|
|
if w == 0.0 {
|
|
continue;
|
|
}
|
|
for (o, v) in out.iter_mut().enumerate() {
|
|
*v += w * samples.get(idx * outputs + o).copied().unwrap_or(0.0);
|
|
}
|
|
}
|
|
for (o, v) in out.iter_mut().enumerate() {
|
|
let (d0, d1) = (decode.get(2 * o).copied().unwrap_or(0.0), decode.get(2 * o + 1).copied().unwrap_or(1.0));
|
|
*v = d0 + *v * (d1 - d0);
|
|
}
|
|
out
|
|
}
|
|
Func::Array(fs) if input.len() > 1 => fs.iter().map(|f| f.eval(input).first().copied().unwrap_or(0.0)).collect(),
|
|
_ => self.eval1(input.first().copied().unwrap_or(0.0)),
|
|
}
|
|
}
|
|
|
|
pub fn eval1(&self, t: f64) -> Vec<f64> {
|
|
match self {
|
|
Func::Exp { domain, c0, c1, n } => {
|
|
let t = t.clamp(domain[0].min(domain[1]), domain[0].max(domain[1]));
|
|
let x = if *n == 1.0 { t } else { t.max(0.0).powf(*n) };
|
|
(0..c0.len().max(c1.len()))
|
|
.map(|i| c0.get(i).copied().unwrap_or(0.0) + x * (c1.get(i).copied().unwrap_or(1.0) - c0.get(i).copied().unwrap_or(0.0)))
|
|
.collect()
|
|
}
|
|
Func::Stitch { domain, funcs, bounds, encode } => {
|
|
if funcs.is_empty() {
|
|
return vec![0.0];
|
|
}
|
|
let t = t.clamp(domain[0], domain[1]);
|
|
let k = bounds.iter().take_while(|b| t >= **b).count().min(funcs.len() - 1);
|
|
let lo = if k == 0 { domain[0] } else { bounds[k - 1] };
|
|
let hi = bounds.get(k).copied().unwrap_or(domain[1]);
|
|
let (e0, e1) = (encode.get(2 * k).copied().unwrap_or(0.0), encode.get(2 * k + 1).copied().unwrap_or(1.0));
|
|
let u = if hi > lo { e0 + (t - lo) / (hi - lo) * (e1 - e0) } else { e0 };
|
|
funcs[k].eval1(u)
|
|
}
|
|
Func::Sampled { domain, size, outputs, encode, decode, samples } => {
|
|
let t = t.clamp(domain[0], domain[1]);
|
|
let e = if domain[1] > domain[0] { encode[0] + (t - domain[0]) / (domain[1] - domain[0]) * (encode[1] - encode[0]) } else { encode[0] };
|
|
let e = e.clamp(0.0, *size as f64 - 1.0);
|
|
let (i0, f) = (e.floor() as usize, e.fract());
|
|
let i1 = (i0 + 1).min(size - 1);
|
|
(0..*outputs)
|
|
.map(|o| {
|
|
let s = samples[i0 * outputs + o] * (1.0 - f) + samples[i1 * outputs + o] * f;
|
|
let (d0, d1) = (decode.get(2 * o).copied().unwrap_or(0.0), decode.get(2 * o + 1).copied().unwrap_or(1.0));
|
|
d0 + s * (d1 - d0)
|
|
})
|
|
.collect()
|
|
}
|
|
Func::Array(fs) => fs.iter().map(|f| f.eval1(t).first().copied().unwrap_or(0.0)).collect(),
|
|
Func::Calc { .. } | Func::SampledN { .. } => self.eval(&[t]),
|
|
Func::Unsupported => vec![0.5],
|
|
}
|
|
}
|
|
|
|
/// Input values worth sampling exactly (stitching bounds).
|
|
pub fn breakpoints(&self) -> Vec<f64> {
|
|
match self {
|
|
Func::Stitch { bounds, .. } => bounds.clone(),
|
|
_ => vec![],
|
|
}
|
|
}
|
|
}
|
|
|
|
/// PDF text string → Rust string (UTF-16BE with a byte-order mark, else PDFDocEncoding ≈ Latin-1).
|
|
pub fn text_string(b: &[u8]) -> String {
|
|
if b.starts_with(&[0xFE, 0xFF]) {
|
|
let u: Vec<u16> = b[2..].as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
|
|
return String::from_utf16_lossy(&u);
|
|
}
|
|
if let Some(rest) = b.strip_prefix(&[0xEF, 0xBB, 0xBF]) {
|
|
return String::from_utf8_lossy(rest).into_owned();
|
|
}
|
|
b.iter().map(|&c| c as char).collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn calculator_functions() {
|
|
// Two inputs → three outputs: { 2 copy mul 3 1 roll add 2 div dup 0.5 gt { pop 1 } if 0 }
|
|
let code = b"{ 2 copy mul 3 1 roll add 2 div dup 0.5 gt { pop 1 } { 0.25 mul } ifelse 0 }";
|
|
let mut lx = crate::object::Lexer::new(code, 0);
|
|
let Some(PsOp::Proc(code)) = ps_parse(&mut lx, 0).into_iter().next() else { panic!() };
|
|
let f = Func::Calc { domain: vec![0.0, 1.0, 0.0, 1.0], range: vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0], code };
|
|
assert_eq!(f.eval(&[0.5, 0.4]), vec![0.2, 0.1125, 0.0]);
|
|
assert_eq!(f.eval(&[1.0, 0.8]), vec![0.8, 1.0, 0.0]);
|
|
}
|
|
}
|