//! PostScript writing primitives: numbers, strings, paths, the prolog's short operator names, and //! the data encodings images and embedded data use (ASCII85, run-length, Flate). use std::fmt::Write; use std::io::{Read, Write as _}; use kurbo::PathEl; use vectorcraft_geom::{Affine, BezPath, FillRule}; /// The procedures the page uses, in `VCdict`. pub(crate) const PROLOG: &str = "/VCdict 64 dict def VCdict begin /bd {bind def} bind def /m {moveto} bd /l {lineto} bd /c {curveto} bd /h {closepath} bd /f {fill} bd /f* {eofill} bd /S {stroke} bd /W {clip newpath} bd /W* {eoclip newpath} bd /q {gsave} bd /Q {grestore} bd /cm {concat} bd /g {setgray} bd /rg {setrgbcolor} bd /k {setcmykcolor} bd /w {setlinewidth} bd /J {setlinecap} bd /j {setlinejoin} bd /M {setmiterlimit} bd /d {setdash} bd /op {setoverprint} bd end "; /// Longest line of encoded data (DSC readers take up to 255 characters). const LINE: usize = 76; /// `v` as PostScript writes it: at most four decimals, no trailing zeros (0 when not finite). pub(crate) fn num(v: f64) -> String { let mut s = String::new(); push_num(&mut s, v); s } /// Append [`num`]`(v)` to `s`. pub(crate) fn push_num(s: &mut String, v: f64) { let v = if v.is_finite() { (v * 10_000.0).round() / 10_000.0 } else { 0.0 }; if v == v.trunc() && v.abs() < 1e15 { let _ = write!(s, "{}", v as i64); return; } let start = s.len(); let _ = write!(s, "{v:.4}"); let keep = s[start..].trim_end_matches('0').trim_end_matches('.').len(); s.truncate(start + keep); } /// `vs` as numbers separated by spaces, then a space. pub(crate) fn push_nums(s: &mut String, vs: &[f64]) { for v in vs { push_num(s, *v); s.push(' '); } } /// `text` as a PostScript string: `(` `)` `\` escaped, anything outside printable ASCII as octal /// escapes of its UTF-8 bytes. pub(crate) fn string(text: &str) -> String { let mut s = String::with_capacity(text.len() + 2); s.push('('); for b in text.bytes() { match b { b'(' | b')' | b'\\' => { s.push('\\'); s.push(char::from(b)); } 0x20..=0x7e => s.push(char::from(b)), _ => { let _ = write!(s, "\\{b:03o}"); } } } s.push(')'); s } /// A matrix operand `[a b c d e f]`. pub(crate) fn matrix(a: Affine) -> String { let mut s = String::from("["); push_nums(&mut s, &a.as_coeffs()); s.pop(); s.push(']'); s } /// Append path `bp` (`m`, `l`, `c`, `h`; quadratics raised to cubics), one segment a line. pub(crate) fn push_path(s: &mut String, bp: &BezPath) { let mut last = kurbo::Point::ZERO; let mut start = kurbo::Point::ZERO; for el in bp.elements() { match *el { PathEl::MoveTo(p) => { push_nums(s, &[p.x, p.y]); s.push_str("m\n"); (last, start) = (p, p); } PathEl::LineTo(p) => { push_nums(s, &[p.x, p.y]); s.push_str("l\n"); last = p; } PathEl::QuadTo(a, p) => { let c1 = last + (a - last) * (2.0 / 3.0); let c2 = p + (a - p) * (2.0 / 3.0); push_nums(s, &[c1.x, c1.y, c2.x, c2.y, p.x, p.y]); s.push_str("c\n"); last = p; } PathEl::CurveTo(a, b, p) => { push_nums(s, &[a.x, a.y, b.x, b.y, p.x, p.y]); s.push_str("c\n"); last = p; } PathEl::ClosePath => { s.push_str("h\n"); last = start; } } } } /// The fill operator for `rule`. pub(crate) fn fill_op(rule: FillRule) -> &'static str { match rule { FillRule::NonZero => "f\n", FillRule::EvenOdd => "f*\n", } } /// The clip operator for `rule` (it also ends the path). pub(crate) fn clip_op(rule: FillRule) -> &'static str { match rule { FillRule::NonZero => "W\n", FillRule::EvenOdd => "W*\n", } } /// `bytes` in ASCII85 with its `~>` end, in lines of about [`LINE`] characters, none starting /// with `%` (DSC readers would take it for a comment). pub(crate) fn ascii85(bytes: &[u8]) -> String { let mut raw = String::with_capacity(bytes.len() * 5 / 4 + 8); let (chunks, rest) = bytes.as_chunks::<4>(); for c in chunks { let v = u32::from_be_bytes(*c); if v == 0 { raw.push('z'); } else { push85(&mut raw, v, 5); } } if !rest.is_empty() { let mut c = [0u8; 4]; for (d, s) in c.iter_mut().zip(rest) { *d = *s; } push85(&mut raw, u32::from_be_bytes(c), rest.len() + 1); } raw.push_str("~>"); let mut out = String::with_capacity(raw.len() + raw.len() / LINE + 2); let mut col = 0; for ch in raw.chars() { if col >= LINE && ch != '%' { out.push('\n'); col = 0; } out.push(ch); col += 1; } out.push('\n'); out } /// The first `n` base-85 digits of `v`. fn push85(s: &mut String, mut v: u32, n: usize) { let mut d = [0u8; 5]; for slot in d.iter_mut().rev() { *slot = (v % 85) as u8 + b'!'; v /= 85; } for b in d.iter().take(n) { s.push(char::from(*b)); } } /// ASCII85 `text` (whitespace ignored, up to `~>`) decoded; `None` when it is malformed. pub(crate) fn ascii85_decode(text: &str) -> Option> { let mut out = Vec::with_capacity(text.len() * 4 / 5); let mut group = [0u8; 5]; let mut n = 0; let mut chars = text.bytes().filter(|b| !b.is_ascii_whitespace()).peekable(); while let Some(b) = chars.next() { match b { b'~' => { if chars.next() != Some(b'>') { return None; } if n == 1 { return None; } if n > 1 { let padded: Vec = group.iter().take(n).copied().chain(std::iter::repeat(b'u')).take(5).collect(); let v = value85(&padded)?; out.extend(v.to_be_bytes().iter().take(n - 1)); } return Some(out); } b'z' if n == 0 => out.extend([0; 4]), b'!'..=b'u' => { *group.get_mut(n)? = b; n += 1; if n == 5 { out.extend(value85(&group)?.to_be_bytes()); n = 0; } } _ => return None, } } None } /// The value of five base-85 digits; `None` past `u32::MAX`. fn value85(digits: &[u8]) -> Option { digits.iter().try_fold(0u32, |acc, d| acc.checked_mul(85)?.checked_add(u32::from(d.checked_sub(b'!')?))) } /// Run-length encoding (PackBits: what PostScript's `RunLengthDecode` reads), shared with TIFF. pub(crate) use vectorcraft_render::encode::tiff::packbits; /// `data` compressed for `FlateDecode` (zlib). pub(crate) fn deflate(data: &[u8]) -> Vec { let mut e = flate2::write::ZlibEncoder::new(Vec::with_capacity(data.len() / 2), flate2::Compression::default()); // Writing to a Vec doesn't fail. let _ = e.write_all(data); e.finish().unwrap_or_default() } /// zlib `data` inflated, when it is valid and at most `max` bytes. pub(crate) fn inflate(data: &[u8], max: u64) -> Option> { let mut out = vec![]; flate2::read::ZlibDecoder::new(data).take(max + 1).read_to_end(&mut out).ok()?; (out.len() as u64 <= max).then_some(out) }