use super::deps;
use super::pgfdim as d;
use crate::Engine;
use crate::cmd::Cmd;
use crate::hash::CsId;
use crate::token::Token;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Model {
Rgb,
Gray,
Cmyk,
}
impl Model {
fn from_name(s: &[u8]) -> Option<Model> {
Some(match s {
b"rgb" => Model::Rgb,
b"gray" => Model::Gray,
b"cmyk" => Model::Cmyk,
_ => return None,
})
}
pub fn name(self) -> &'static [u8] {
match self {
Model::Rgb => b"rgb",
Model::Gray => b"gray",
Model::Cmyk => b"cmyk",
}
}
fn arity(self) -> usize {
match self {
Model::Rgb => 3,
Model::Gray => 1,
Model::Cmyk => 4,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Spec {
pub model: Model,
pub comps: Vec<Vec<u8>>,
}
fn number(s: &[u8]) -> bool {
let s = s.strip_prefix(b"-").unwrap_or(s);
let digits = s.iter().filter(|c| c.is_ascii_digit()).count();
let points = s.iter().filter(|&&c| c == b'.').count();
digits > 0 && points <= 1 && digits + points == s.len() && s.last() != Some(&b'.')
}
fn components(model: Model, s: &[u8]) -> Option<Vec<Vec<u8>>> {
let comps: Vec<Vec<u8>> = s.split(|&c| c == b',').map(<[u8]>::to_vec).collect();
(comps.len() == model.arity() && comps.iter().all(|c| number(c))).then_some(comps)
}
fn group<'a>(toks: &'a [Token], i: &mut usize) -> Option<&'a [Token]> {
let t = *toks.get(*i)?;
if t.is_cs() || t.cmd_code() != Cmd::LeftBrace as u32 {
return None;
}
let start = *i + 1;
let mut depth = 0;
loop {
let k = *toks.get(*i)?;
if !k.is_cs() {
if k.cmd_code() == Cmd::LeftBrace as u32 {
depth += 1;
} else if k.cmd_code() == Cmd::RightBrace as u32 {
depth -= 1;
if depth == 0 {
*i += 1;
return Some(&toks[start..*i - 1]);
}
}
}
*i += 1;
}
}
pub fn xcolor_args(e: &Engine, cs: CsId) -> Option<[Vec<Token>; 4]> {
let xcolor_at = e.hash.lookup(b"xcolor@")?;
let body = deps::plain_body(e, cs)?;
if e.eqtb.meaning(cs).cmd != Cmd::Call || body.first() != Some(&Token::cs(xcolor_at)) {
return None;
}
let mut i = 1;
let mut out: [Vec<Token>; 4] = Default::default();
for slot in &mut out {
*slot = group(body, &mut i)?.to_vec();
}
(i == body.len()).then_some(out)
}
pub fn named_spec(e: &Engine, name: &[u8]) -> Option<Spec> {
let mut cname = b"\\color@".to_vec();
cname.extend_from_slice(name);
let cs = e.hash.lookup(&cname)?;
let [_, _, m, c] = xcolor_args(e, cs)?;
let model = Model::from_name(&deps::sanitized(&deps::edef(e, &m)?)?)?;
let clr = deps::sanitized(&deps::edef(e, &c)?)?;
Some(Spec {
model,
comps: components(model, &clr)?,
})
}
fn shortcut(e: &Engine, model: Model, name: &[u8]) -> Option<Option<Spec>> {
let mut n = b"XC@clr@".to_vec();
n.extend_from_slice(model.name());
n.push(b'@');
n.extend_from_slice(name);
let Some(cs) = e.hash.lookup(&n) else {
return Some(None);
};
let m = e.eqtb.meaning(cs);
if m.cmd == Cmd::UndefinedCs {
return Some(None);
}
let relax = e.hash.lookup(b"relax").map(|r| e.eqtb.meaning(r))?;
if deps::absent(m, relax) {
return Some(None);
}
let text = deps::sanitized(deps::plain_body(e, cs)?)?;
Some(Some(Spec {
model,
comps: components(model, &text)?,
}))
}
fn strip_pt(x: i32) -> Vec<u8> {
let s = d::print(x);
match s.strip_suffix(b".0") {
Some(int) => int.to_vec(),
None => s,
}
}
fn rshift(x: i32) -> Option<i32> {
let s = d::print(x);
let (int, frac) = d::split_point(&s);
let (&last, head) = int.split_last()?;
let mut t = head.to_vec();
t.push(b'.');
t.push(last);
t.extend_from_slice(frac);
d::scan_pt(&t).ok()
}
pub fn calc_n(x: &[u8]) -> Vec<u8> {
let (int, frac) = match x.iter().position(|&c| c == b'.') {
Some(p) => (&x[..p], &x[p + 1..]),
None => (x, &b"0"[..]),
};
let int_value = |extra: u8| -> i64 {
let mut t = int.to_vec();
t.push(extra);
d::scan_int(&t).unwrap_or(0)
};
let r: Vec<u8> = if int_value(b'0') > 0 {
b"1.00000".to_vec()
} else if int_value(b'1') < 0 {
b"0.00000".to_vec()
} else {
let mut f = frac.to_vec();
f.extend_from_slice(b"00000");
let mut r = b"0.".to_vec();
r.extend_from_slice(&f[..5]);
r
};
let (i, f) = r.split_at(1);
let mut tail = f[1..].to_vec();
tail.extend_from_slice(b"00000");
let end = tail.windows(5).position(|w| w == b"00000").unwrap_or(0);
let digits = &tail[..end];
let mut out = i.to_vec();
if digits.iter().any(|&c| c != b'0') {
out.push(b'.');
out.extend_from_slice(digits);
}
out
}
fn from_gray(g: &[u8], to: Model) -> Option<Vec<Vec<u8>>> {
match to {
Model::Gray => Some(vec![g.to_vec()]),
Model::Rgb => {
let n = calc_n(g);
Some(vec![n.clone(), n.clone(), n])
}
Model::Cmyk => None,
}
}
fn mix(cur: &[Vec<u8>], other: &[Vec<u8>], p: i32) -> Option<Vec<Vec<u8>>> {
let tempdima = p;
let tempdimb = d::adv(d::scan_pt(b"100").ok()?, -tempdima).ok()?;
cur.iter()
.zip(other)
.map(|(c, m)| {
let mut x = d::scan_factor(c, tempdima).ok()?;
x = d::adv(x, d::scan_factor(m, tempdimb).ok()?).ok()?;
x = rshift(rshift(x)?)?;
Some(strip_pt(x))
})
.collect()
}
pub fn split(e: &Engine, expr: &[u8]) -> Option<Spec> {
split_over(e, expr, None)
}
pub fn split_over(e: &Engine, expr: &[u8], over: Option<(&[u8], &Spec)>) -> Option<Spec> {
let named = |n: &[u8]| match over {
Some((name, spec)) if name == n => Some(spec.clone()),
_ => named_spec(e, n),
};
if expr.iter().any(|c| b">:-[]+/".contains(c)) {
return None; }
let mut parts = expr.split(|&c| c == b'!');
let name = parts.next()?;
let mut spec = named(name)?;
let rest: Vec<&[u8]> = parts.collect();
let mut k = 0;
while k < rest.len() {
let pct = rest[k];
let other = rest.get(k + 1).copied().unwrap_or(b"");
k += 2;
if pct.is_empty() || !number(pct) || pct.starts_with(b"-") {
return None;
}
let one = d::scan_pt(b"1").ok()?;
let p = d::scan_factor(pct, one).ok()?;
if p <= 0 || p >= d::scan_factor(b"100", one).ok()? {
return None; }
let other = if other.is_empty() {
&b"white"[..]
} else {
other
};
let other_spec = match shortcut(e, spec.model, other)? {
Some(s) => s,
None => {
let o = named(other)?;
if spec.model == Model::Gray {
let g = spec.comps[0].clone();
spec = Spec {
model: o.model,
comps: from_gray(&g, o.model)?,
};
o
} else if o.model == spec.model {
o
} else if o.model == Model::Gray {
Spec {
model: spec.model,
comps: from_gray(&o.comps[0], spec.model)?,
}
} else {
return None;
}
}
};
spec.comps = mix(&spec.comps, &other_spec.comps, p)?;
}
Some(spec)
}
pub fn normalized(spec: &Spec) -> Spec {
Spec {
model: spec.model,
comps: spec.comps.iter().map(|c| calc_n(c)).collect(),
}
}
pub fn in_range(spec: &Spec) -> bool {
let one = 65536;
spec.comps
.iter()
.all(|c| d::scan_pt(c).is_ok_and(|v| (0..=one).contains(&v)))
}
pub fn last_arg_dimen(spec: &Spec) -> Option<i32> {
let last = match spec.model {
Model::Cmyk => &spec.comps[2],
_ => spec.comps.last()?,
};
let v = d::scan_factor(last, 65536).ok()?;
Some(if v < 0 { 0o7777777777 } else { v })
}
pub fn driver_text(spec: &Spec) -> Vec<Token> {
let mut out = Vec::new();
let chars = |s: &[u8], out: &mut Vec<Token>| {
out.extend(s.iter().map(|&c| Token::other(u32::from(c))));
};
let letters = |s: &[u8], out: &mut Vec<Token>| {
out.extend(s.iter().map(|&c| Token::letter(u32::from(c))));
};
for op in [&b"rg"[..], b"RG"] {
if !out.is_empty() {
out.push(Token::SPACE);
}
for c in &spec.comps {
chars(c, &mut out);
out.push(Token::SPACE);
}
let op: &[u8] = match spec.model {
Model::Rgb => op,
Model::Gray => {
if op == b"rg" {
b"g"
} else {
b"G"
}
}
Model::Cmyk => {
if op == b"rg" {
b"k"
} else {
b"K"
}
}
};
letters(op, &mut out);
}
out
}
pub fn clr_tokens(spec: &Spec) -> Vec<Token> {
let mut out = Vec::new();
for (k, c) in spec.comps.iter().enumerate() {
if k > 0 {
out.push(Token::other(u32::from(b',')));
}
out.extend(c.iter().map(|&b| Token::other(u32::from(b))));
}
out
}
pub fn definition(e: &Engine, spec: &Spec) -> Option<Vec<Token>> {
let xcolor_at = e.hash.lookup(b"xcolor@")?;
let n = normalized(spec);
let mut out = vec![
Token::END_MATCH,
Token::cs(xcolor_at),
Token::LEFT_BRACE,
Token::RIGHT_BRACE,
Token::LEFT_BRACE,
];
out.extend(driver_text(&n));
out.push(Token::RIGHT_BRACE);
out.push(Token::LEFT_BRACE);
out.extend(n.model.name().iter().map(|&c| Token::other(u32::from(c))));
out.push(Token::RIGHT_BRACE);
out.push(Token::LEFT_BRACE);
out.extend(clr_tokens(&n));
out.push(Token::RIGHT_BRACE);
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calc_n_cases() {
let n = |s: &str| String::from_utf8(calc_n(s.as_bytes())).unwrap();
assert_eq!(n("1"), "1");
assert_eq!(n("0"), "0");
assert_eq!(n("0.5"), "0.5");
assert_eq!(n(".3"), "0.3");
assert_eq!(n("0.123456"), "0.12345");
assert_eq!(n("0.10010"), "0.1001");
assert_eq!(n("0.005"), "0.005");
assert_eq!(n("1.5"), "1");
assert_eq!(n("-0.5"), "0");
assert_eq!(n("2"), "1");
}
#[test]
fn mixing() {
let p = d::scan_pt(b"50").unwrap();
let c = vec![b"1".to_vec(), b"0".to_vec(), b"0".to_vec()];
let w = vec![b"1".to_vec(), b"1".to_vec(), b"1".to_vec()];
let m = mix(&c, &w, p).unwrap();
assert_eq!(m, vec![b"1".to_vec(), b"0.5".to_vec(), b"0.5".to_vec()]);
}
}