Merge #1909: EPS: Illustrator EPS files draw their artwork (#955) (@JWesCampbell)

This commit is contained in:
aiongg
2026-10-24 01:21:39 +00:00
2 changed files with 121 additions and 1 deletions
+101 -1
View File
@@ -467,6 +467,10 @@ impl Interp {
if let Some(v) = self.lookup(n) {
return match v {
V::Proc(_) => self.exec_obj(v, src),
// An operator stored under another name (`/mo /moveto load def`, as Illustrator's
// EPS prolog does for every path operator) runs that operator. It pushed the
// operator's name instead, so such files drew nothing (#955).
V::Name(op) => self.builtin(&op, src),
other => {
self.push(other);
Ok(())
@@ -1203,6 +1207,96 @@ impl Interp {
}
}
"currentglobal" | "currentpacking" | "currentshared" => self.push(V::Bool(false)),
// Operators that return values must push them, or the rest of the program reads a
// shifted stack: Illustrator's EPS prolog queries the device this way and then drew
// nothing (#955). Values are a plain device's defaults (PostScript Language
// Reference, 3rd ed., chapter 8).
"currentflat" => self.push(V::Num(1.0)),
"currentscreen" => {
self.push(V::Num(60.0));
self.push(V::Num(45.0));
self.push(V::Proc(Rc::new(vec![])));
}
"currentblackgeneration" | "currentundercolorremoval" | "currenttransfer" => self.push(V::Proc(Rc::new(vec![]))),
"currentcolortransfer" => {
for _ in 0..4 {
self.push(V::Proc(Rc::new(vec![])));
}
}
"currentpagedevice" | "currenthalftone" | "currentsystemparams" | "currentuserparams" | "currentdevparams" => {
if n == "currentdevparams" {
self.pop();
}
self.push(V::Dict(new_dict()));
}
"setuserparams" | "setsystemparams" | "setcolortransfer" | "setblackgeneration" | "setundercolorremoval" => {
let k = if n == "setcolortransfer" { 4 } else { 1 };
for _ in 0..k {
self.pop();
}
}
// `file status` → false; `string status` → false (no such file).
"status" => {
self.pop();
self.push(V::Bool(false));
}
"dictstack" => {
self.pop();
let ds: Vec<V> = self.dicts.iter().map(|d| V::Dict(d.clone())).collect();
self.push(array(ds));
}
"maxlength" => {
let k = match self.pop() {
V::Dict(d) => d.borrow().len(),
_ => 0,
};
self.push(V::Num(k.saturating_add(16) as f64));
}
"internaldict" => {
self.pop();
let d = self.dicts[0].clone();
self.push(V::Dict(d));
}
"undefineresource" => {
self.pop();
self.pop();
}
// `[ … /Name pdfmark`: a Distiller annotation; other interpreters discard it.
"pdfmark" => {
while let Some(v) = self.st.pop() {
if matches!(v, V::Mark) {
break;
}
}
}
// `source [params] /Name filter`. A SubFileDecode of the current file with a count of
// 0 reads to its end-of-data string: skip that data (an embedded XMP packet, which
// otherwise ran as PostScript). Other filters stand for the file they read.
"filter" => {
let name = self.pop().key();
if name == "SubFileDecode" {
let eod = match self.pop() {
V::Str(s) => s.borrow().clone(),
_ => vec![],
};
let count = self.popn();
if let V::File = self.pop()
&& count == 0.0
&& !eod.is_empty()
{
src.pos = match find_from(src.d, &eod, src.pos) {
Some(p) => p.saturating_add(eod.len()),
None => src.d.len(),
};
}
} else {
if matches!(self.st.last(), Some(V::Dict(_))) {
self.pop();
}
self.pop();
}
self.push(V::File);
}
"vmstatus" => {
for _ in 0..3 {
self.push(V::Num(0.0));
@@ -1236,9 +1330,15 @@ impl Interp {
let a: Vec<f64> = self.pop().items().unwrap_or_default().iter().filter_map(V::num).collect();
self.gs.dash = (!a.is_empty() && a.iter().any(|v| *v > 0.0)).then_some((a, off));
}
"setflat" | "setstrokeadjust" | "setoverprint" | "setrenderingintent" | "setcolorrendering" | "sethalftone" | "setscreen" | "settransfer" => {
"setflat" | "setstrokeadjust" | "setoverprint" | "setrenderingintent" | "setcolorrendering" | "sethalftone" | "settransfer" => {
self.pop();
}
// frequency angle proc
"setscreen" => {
for _ in 0..3 {
self.pop();
}
}
"setgray" => {
let g = self.popn().clamp(0.0, 1.0);
self.gs.color = [g; 3];
+20
View File
@@ -25,6 +25,26 @@ fn ink_x(img: &effectcraft_raster::Image) -> (i64, i64) {
(x0, x1)
}
/// #955: EPS files that name their operators (`/mo /moveto load def`, as Illustrator's do),
/// query the device and embed metadata read through a SubFileDecode filter draw their artwork.
/// They imported as an empty layer.
#[test]
fn eps_with_aliased_operators_device_queries_and_embedded_data_draws() {
let square = "1 0 0 setrgbcolor 50 25 mo 150 25 li 150 75 li 50 75 li cp f\n";
let aliases = "/mo /moveto load def /li /lineto load def /cp /closepath load def /f /fill load def\n";
let doc = eps(format!("{aliases}{square}").as_bytes());
assert!(coverage(&render(&doc), 50, 25, 150, 75) > 4900.0, "aliased operators draw the square");
// Every operator that returns a value pushes it, so the stack stays aligned.
let queries = "mark currentflat currentscreen currentpagedevice currenthalftone currentblackgeneration \
currentundercolorremoval currentcolortransfer currentsystemparams counttomark 13 eq /ok exch def cleartomark\n";
let doc = eps(format!("{aliases}{queries}ok {{ {square} }} if\n").as_bytes());
assert!(coverage(&render(&doc), 50, 25, 150, 75) > 4900.0, "device queries leave 13 values");
// Data up to the end-of-data string isn't run: its unbalanced brace would swallow the rest.
let data = "currentfile 0 (%END-OF-DATA) /SubFileDecode filter pop\n<x> { ( not PostScript\n%END-OF-DATA\n";
let doc = eps(format!("{aliases}{data}{square}").as_bytes());
assert!(coverage(&render(&doc), 50, 25, 150, 75) > 4900.0, "drawing after the embedded data");
}
#[test]
fn eps_text_with_bundled_fonts_and_show_variants() {
// Not embedded: drawn with the bundled stand-in, like the same text in a PDF.