From cb5b06672fbdaf2f4cf2ac526227a2be838af336 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 14 Nov 2026 01:28:35 +0000 Subject: [PATCH] EPS: delegate binary preview header, sections, and ASCII85 to craft-eps --- Cargo.lock | 15 ++++++- crates/eps/Cargo.toml | 1 + crates/eps/src/lib.rs | 51 +++--------------------- crates/eps/src/ps.rs | 91 +------------------------------------------ 4 files changed, 22 insertions(+), 136 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4feae9991..7af992cadb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -978,6 +978,11 @@ dependencies = [ "ruzstd", ] +[[package]] +name = "craft-codepage" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=6d620e971e820b517b11a99cad040ebac670e1e2#6d620e971e820b517b11a99cad040ebac670e1e2" + [[package]] name = "craft-docpath" version = "0.1.0" @@ -986,13 +991,20 @@ source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7 [[package]] name = "craft-dxf" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=119415f46f601e20b9bc6b1b4d6979ceda86d88e#119415f46f601e20b9bc6b1b4d6979ceda86d88e" +source = "git+https://github.com/storytold/craft-libs?rev=6d620e971e820b517b11a99cad040ebac670e1e2#6d620e971e820b517b11a99cad040ebac670e1e2" dependencies = [ + "craft-codepage", "encoding_rs", + "kurbo 0.13.1", "log", "thiserror 2.0.21", ] +[[package]] +name = "craft-eps" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=6d620e971e820b517b11a99cad040ebac670e1e2#6d620e971e820b517b11a99cad040ebac670e1e2" + [[package]] name = "craft-fonts-build" version = "0.1.0" @@ -6548,6 +6560,7 @@ dependencies = [ name = "vectorcraft-eps" version = "0.24.1" dependencies = [ + "craft-eps", "craft-pdf", "encoding_rs", "fax", diff --git a/crates/eps/Cargo.toml b/crates/eps/Cargo.toml index 13c1c76ada..bd173588aa 100644 --- a/crates/eps/Cargo.toml +++ b/crates/eps/Cargo.toml @@ -9,6 +9,7 @@ description = "VectorCraft Encapsulated PostScript: a hand-written EPS writer (P workspace = true [dependencies] +craft-eps = { workspace = true } vectorcraft-geom = { workspace = true } vectorcraft-color = { workspace = true } vectorcraft-doc = { workspace = true } diff --git a/crates/eps/src/lib.rs b/crates/eps/src/lib.rs index 44ec49c190..49b64f4bce 100644 --- a/crates/eps/src/lib.rs +++ b/crates/eps/src/lib.rs @@ -352,64 +352,23 @@ fn read_data(ps: &[u8], name: &str) -> Option> { None } -/// The magic number of an EPS file with a binary header (a preview). -const DOS_MAGIC: [u8; 4] = [0xC5, 0xD0, 0xD3, 0xC6]; -/// The binary header's size. -const DOS_HEADER: usize = 30; +pub use craft_eps::{DOS_HEADER, DOS_MAGIC, Sections, is_eps}; /// `ps` behind a binary header, followed by the TIFF preview `tiff`. fn dos_eps(ps: &[u8], tiff: &[u8]) -> Result, String> { - let big = || "the file is too large for an EPS preview header (4 GB): choose no preview".to_string(); - let ps_len = u32::try_from(ps.len()).map_err(|_| big())?; - let tiff_at = u32::try_from(DOS_HEADER + ps.len()).map_err(|_| big())?; - let tiff_len = u32::try_from(tiff.len()).map_err(|_| big())?; - tiff_at.checked_add(tiff_len).ok_or_else(big)?; - let mut out = Vec::with_capacity(DOS_HEADER + ps.len() + tiff.len()); - out.extend(DOS_MAGIC); - out.extend((DOS_HEADER as u32).to_le_bytes()); - out.extend(ps_len.to_le_bytes()); - // No metafile preview. - out.extend(0u32.to_le_bytes()); - out.extend(0u32.to_le_bytes()); - out.extend(tiff_at.to_le_bytes()); - out.extend(tiff_len.to_le_bytes()); - // No checksum. - out.extend(0xFFFFu16.to_le_bytes()); - out.extend(ps); - out.extend(tiff); - Ok(out) + craft_eps::wrap(ps, Some(tiff), None).map_err(|e| e.to_string()) } /// The sections of an EPS file: its PostScript and its TIFF preview (binary header), or the whole /// file as PostScript. `None` when a binary header points outside the file. pub fn sections(bytes: &[u8]) -> Option<(&[u8], Option<&[u8]>)> { - if !bytes.starts_with(&DOS_MAGIC) { - return Some((bytes, None)); - } - let word = |at: usize| bytes.get(at..at + 4).and_then(|b| <[u8; 4]>::try_from(b).ok()).map(|b| u32::from_le_bytes(b) as usize); - let part = |at: usize| { - let (start, len) = (word(at)?, word(at + 4)?); - bytes.get(start..start.checked_add(len)?) - }; - let ps = part(4)?; - let tiff = match word(24) { - Some(0) | None => None, - Some(_) => Some(part(20)?), - }; - Some((ps, tiff)) + let s = craft_eps::sections(bytes).ok()?; + Some((s.postscript, s.tiff)) } /// The Windows metafile preview behind an EPS file's binary header, if it has one. pub fn metafile_preview(bytes: &[u8]) -> Option<&[u8]> { - if !bytes.starts_with(&DOS_MAGIC) { - return None; - } - let word = |at: usize| bytes.get(at..at + 4).and_then(|b| <[u8; 4]>::try_from(b).ok()).map(|b| u32::from_le_bytes(b) as usize); - let (start, len) = (word(12)?, word(16)?); - if start == 0 || len == 0 { - return None; - } - bytes.get(start..start.checked_add(len)?) + craft_eps::sections(bytes).ok()?.metafile } /// The native document an EPS file written by [`export`] carries, if any. diff --git a/crates/eps/src/ps.rs b/crates/eps/src/ps.rs index 88da08551e..d3d0dfcf4e 100644 --- a/crates/eps/src/ps.rs +++ b/crates/eps/src/ps.rs @@ -180,54 +180,7 @@ pub(crate) fn clip_op(rule: FillRule) -> &'static str { /// `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); - if raw.starts_with('%') { - // Whitespace is ignored by readers, and keeps the first line from starting with `%`. - out.push(' '); - } - 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)); - } -} +pub(crate) use craft_eps::ascii85::encode as ascii85; /// Most bytes [`ascii85_decode`] makes: what one filter or data section may hold. pub(crate) const MAX_ASCII85: usize = 1 << 28; @@ -241,47 +194,7 @@ pub(crate) fn ascii85_decode(text: &str) -> Option> { /// [`ascii85_decode`] of at most `max` bytes: `z` is four bytes for one, so the length of the /// text doesn't bound what it becomes (#1715). pub(crate) fn ascii85_decode_within(text: &str, max: usize) -> Option> { - let mut out = Vec::with_capacity((text.len() / 5 * 4).min(max)); - 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() { - if out.len() > max { - return None; - } - 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 (out.len() <= max).then_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'!')?))) + craft_eps::ascii85::decode(text.as_bytes(), max) } /// Run-length encoding (PackBits: what PostScript's `RunLengthDecode` reads), shared with TIFF.