mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
Fix #505: Illustrator EPS type with drop shadows and glows opens as vectors
Text effects from stock sites are Illustrator EPS with live type whose appearance (several fills and strokes, offset paths, drop shadows, glows) is flattened into many small images and clipped type. Made here with Illustrator 2026 (graphic styles and live effects on type, live and outlined, formats 2026, CS6, 10 and 8), every one fell back to its preview: - Illustrator's procsets keep their own graphics-state stack in a dictionary, count each `save` in it and raise `limitcheck` at 64, relying on `restore` to put the count back. `restore` didn't undo dictionary changes, so a file with more than 64 images (each drawn in a save) stopped. Now `restore` puts dictionaries back as they were at the `save`, as restoring virtual memory does: while a save is open, `def`, `store`, `put`, `undef` and dictionary `copy` keep the old value in a journal (counted against the memory budget), undone newest first. A save that was restored already (or through an outer one) can't be restored again (`invalidrestore`). - The Illustrator 8 format's pattern fills divide by the font cache's largest glyph (`cachestatus`), which was 0: it now reports a printer's sizes. - Type in Illustrator's reencoded font copies (`/Impact*1`) was set in the fallback font: the `*1` suffix is dropped when the family is looked up. The images of those effects also came in banded on main; that is #498's fix, which this builds on. Tests: 100 saves counted in a dictionary and restored, every dictionary change undone, saves restored twice, `cachestatus`, reencoded font names. The import fuzz vocabulary has saves and restores around dictionary changes. The generated files (not committed) now open with no warnings and look like their previews; the Illustrator 8-format ones draw what their PostScript holds (that format writes effects as opaque rasters). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a303add775
commit
262caec2d6
+2
-2
File diff suppressed because one or more lines are too long
@@ -953,7 +953,9 @@ fn arb_ps_token() -> impl Strategy<Value = String> {
|
||||
"/f currentfile /ASCIIHexDecode filter def", "{f 3 string readstring pop}", "{f 0 string readstring pop}", "true 3 colorimage",
|
||||
"3 2 8 [3 0 0 2 0 0] {f 5 string readstring pop} {(ab)} {f 1 string readstring pop} true 3 colorimage",
|
||||
"<< /ImageType 1 /Width 3 /Height 2 /BitsPerComponent 4 /Decode [0 1 0 1 0 1 0 1] /ImageMatrix [3 0 0 2 0 0] /MultipleDataSources true /DataSource [(a) (bc) {f 2 string readstring pop} (d)] >>",
|
||||
"(>>) cvx", "(1 2 add) cvx exec", "(x) cvx dup exec", "0 load", "1 { } def", "(mark) cvx cvlit"
|
||||
// Virtual memory restored: dictionary changes undone, saves restored twice.
|
||||
"/a 1 def save /a 2 def", "restore", "save dup restore", "1 dict save exch /k 1 put", "cachestatus", "/n 0 def save /n n 1 add store",
|
||||
"(>>) cvx", "(1 2 add) cvx exec", "(x) cvx dup exec", "0 load", "1 { } def", "(mark) cvx cvlit"
|
||||
])
|
||||
.prop_map(str::to_string),
|
||||
]
|
||||
|
||||
@@ -49,6 +49,11 @@ pub(crate) struct Interp<'a> {
|
||||
/// Bytes asked for so far (see [`MAX_MEMORY`]).
|
||||
allocated: usize,
|
||||
seed: u32,
|
||||
/// The `save`s not restored yet, oldest first.
|
||||
saves: Vec<SaveMark>,
|
||||
/// What dictionary entries were before they changed since the oldest `save` (dictionary,
|
||||
/// key, old value): `restore` puts them back, as restoring virtual memory does.
|
||||
journal: Vec<(DictRef, Key, Option<Obj>)>,
|
||||
/// Where the error that is unwinding was raised (see [`Fault`]).
|
||||
pub fault: Option<Fault>,
|
||||
/// The width a Type 3 glyph procedure gave (`setcachedevice`, `setcharwidth`).
|
||||
@@ -72,6 +77,13 @@ pub(crate) struct Fault {
|
||||
/// Most procedure names a [`Fault`] keeps.
|
||||
const FAULT_PROCS: usize = 3;
|
||||
|
||||
/// Where a `save` was made: the graphics states saved and the journal's length then.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct SaveMark {
|
||||
gstates: usize,
|
||||
journal: usize,
|
||||
}
|
||||
|
||||
fn new_dict() -> DictRef {
|
||||
Rc::new(RefCell::new(Dict::new()))
|
||||
}
|
||||
@@ -143,6 +155,8 @@ impl<'a> Interp<'a> {
|
||||
ops: 0,
|
||||
allocated: 0,
|
||||
seed: 1,
|
||||
saves: vec![],
|
||||
journal: vec![],
|
||||
fault: None,
|
||||
glyph_width: None,
|
||||
g,
|
||||
@@ -190,6 +204,7 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
self.dicts.clear();
|
||||
self.stack.clear();
|
||||
self.journal.clear();
|
||||
}
|
||||
|
||||
// ---------- the operand stack ----------
|
||||
@@ -879,7 +894,7 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
Obj::Dict(d) => {
|
||||
let key = k.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
insert(&d, key, v)?;
|
||||
self.insert(&d, key, Some(v))?;
|
||||
true
|
||||
}
|
||||
_ => return ps_err("typecheck", ""),
|
||||
@@ -968,7 +983,7 @@ impl<'a> Interp<'a> {
|
||||
let v = self.pop()?;
|
||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
||||
insert(&d, k, v)?;
|
||||
self.insert(&d, k, Some(v))?;
|
||||
}
|
||||
Load => {
|
||||
let k = self.pop()?;
|
||||
@@ -981,7 +996,7 @@ impl<'a> Interp<'a> {
|
||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
let d = self.dicts.iter().rev().find(|d| d.borrow().contains_key(&k)).or(self.dicts.last()).cloned();
|
||||
let d = d.ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
||||
insert(&d, k, v)?;
|
||||
self.insert(&d, k, Some(v))?;
|
||||
}
|
||||
Known => {
|
||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
@@ -1001,7 +1016,8 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
Undef => {
|
||||
let k = self.pop()?.key().ok_or(PsError::Ps("typecheck", String::new()))?;
|
||||
self.pop_dict()?.borrow_mut().remove(&k);
|
||||
let d = self.pop_dict()?;
|
||||
self.insert(&d, k, None)?;
|
||||
}
|
||||
CurrentDict => {
|
||||
let d = self.dicts.last().cloned().ok_or(PsError::Ps("dictstackunderflow", String::new()))?;
|
||||
@@ -1034,11 +1050,23 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
Save => {
|
||||
self.gsave()?;
|
||||
self.push(Obj::Save(self.saved.len()))?;
|
||||
self.push(Obj::Save(self.saves.len()))?;
|
||||
self.saves.push(SaveMark { gstates: self.saved.len(), journal: self.journal.len() });
|
||||
}
|
||||
Restore => {
|
||||
let Obj::Save(level) = self.pop()? else { return ps_err("typecheck", "") };
|
||||
while self.saved.len() >= level && !self.saved.is_empty() {
|
||||
let Obj::Save(i) = self.pop()? else { return ps_err("typecheck", "") };
|
||||
// A save restored already (or through an outer one) can't be restored again.
|
||||
let mark = self.saves.get(i).copied().ok_or(PsError::Ps("invalidrestore", String::new()))?;
|
||||
self.saves.truncate(i);
|
||||
// The dictionaries as they were at the save, newest change undone first.
|
||||
for (d, k, old) in self.journal.drain(mark.journal..).rev() {
|
||||
let mut d = d.borrow_mut();
|
||||
match old {
|
||||
Some(v) => d.insert(k, v),
|
||||
None => d.remove(&k),
|
||||
};
|
||||
}
|
||||
while self.saved.len() >= mark.gstates && !self.saved.is_empty() {
|
||||
self.grestore();
|
||||
}
|
||||
}
|
||||
@@ -1083,9 +1111,11 @@ impl<'a> Interp<'a> {
|
||||
self.push(Obj::Int(1 << 20))?;
|
||||
}
|
||||
}
|
||||
// An empty font cache with a printer's sizes: bytes used and most, fonts used and most,
|
||||
// glyphs used and most, and the largest glyph cached (programs divide by it).
|
||||
CacheStatus => {
|
||||
for _ in 0..7 {
|
||||
self.push(Obj::Int(0))?;
|
||||
for v in [0, 1 << 20, 0, 1000, 0, 10_000, 25_000] {
|
||||
self.push(Obj::Int(v))?;
|
||||
}
|
||||
}
|
||||
StartJob => {
|
||||
@@ -1143,7 +1173,9 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
Obj::Dict(d) => {
|
||||
let from = self.pop_dict()?.borrow().clone();
|
||||
d.borrow_mut().extend(from);
|
||||
for (k, v) in from {
|
||||
self.insert(&d, k, Some(v))?;
|
||||
}
|
||||
self.push(Obj::Dict(d))?;
|
||||
}
|
||||
_ => return ps_err("typecheck", "copy"),
|
||||
@@ -1219,6 +1251,26 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Put `v` under `k` in `d` (`None`: remove it), within [`MAX_ALLOC`] entries; while a `save`
|
||||
/// is open the old value is kept for `restore`.
|
||||
fn insert(&mut self, d: &DictRef, k: Key, v: Option<Obj>) -> Res {
|
||||
let old = {
|
||||
let mut dm = d.borrow_mut();
|
||||
if v.is_some() && dm.len() >= MAX_ALLOC && !dm.contains_key(&k) {
|
||||
return Err(PsError::Limit("a dictionary grew too large"));
|
||||
}
|
||||
match v {
|
||||
Some(v) => dm.insert(k.clone(), v),
|
||||
None => dm.remove(&k),
|
||||
}
|
||||
};
|
||||
if !self.saves.is_empty() {
|
||||
self.alloc(2 * std::mem::size_of::<Obj>())?;
|
||||
self.journal.push((d.clone(), k, old));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------- resources ----------
|
||||
|
||||
/// The resources of `category`.
|
||||
@@ -1293,16 +1345,6 @@ impl<'a> Interp<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Put `v` under `k` in `d`, within [`MAX_ALLOC`] entries.
|
||||
fn insert(d: &DictRef, k: Key, v: Obj) -> Res {
|
||||
let mut d = d.borrow_mut();
|
||||
if d.len() >= MAX_ALLOC && !d.contains_key(&k) {
|
||||
return Err(PsError::Limit("a dictionary grew too large"));
|
||||
}
|
||||
d.insert(k, v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An array or string index.
|
||||
fn index(k: &Obj) -> Res<usize> {
|
||||
match *k {
|
||||
|
||||
@@ -280,6 +280,10 @@ fn type_becomes_point_type_in_the_font_named() {
|
||||
assert!(t2.xf.translation().x > 15.0);
|
||||
assert_eq!(crate::family_style("ABCDEF+TimesNewRomanPS-BoldItalicMT"), ("Times New Roman".into(), "Bold Italic".into()));
|
||||
assert_eq!(crate::family_style("Times-Roman"), ("Times".into(), "Regular".into()));
|
||||
// Illustrator's reencoded copies (#505).
|
||||
assert_eq!(crate::family_style("Impact*1"), ("Impact".into(), "Regular".into()));
|
||||
assert_eq!(crate::family_style("MyriadPro-Bold*12"), ("Myriad Pro".into(), "Bold".into()));
|
||||
assert_eq!(crate::family_style("Odd*Name"), ("Odd*Name".into(), "Regular".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -991,3 +991,27 @@ showpage"##;
|
||||
assert!(all(d).iter().any(|n| fill(n).and_then(Paint::color) == Some(Color::rgb(0.0, 1.0, 0.0))));
|
||||
assert_eq!(objects(d).len(), 3);
|
||||
}
|
||||
|
||||
/// `restore` puts dictionaries back as they were at the `save`, as restoring virtual memory does:
|
||||
/// Illustrator's procsets count `save`s in a dictionary and raise `limitcheck` at 64, so every file
|
||||
/// with more than 64 images (the rasters of drop shadows and glows on type, #505) fell back to
|
||||
/// its preview while the count only grew.
|
||||
#[test]
|
||||
fn restore_puts_dictionaries_back() {
|
||||
let r = read(
|
||||
"/AGM 4 dict def AGM /n 0 put \
|
||||
/mysave { save AGM /n get 1 add dup 64 ge { limitcheck } if AGM exch /n exch put } def \
|
||||
1 1 100 { pop mysave 0 0 1 1 rectfill restore } for AGM /n get 0 eq { 10 10 5 5 rectfill } if",
|
||||
);
|
||||
clean(&r);
|
||||
assert_eq!(objects(&r.document).len(), 101);
|
||||
// def, store, put, undef and copy are undone, newest first; a save restored can't be again.
|
||||
super::tests::check(
|
||||
"/a 1 def /d 2 dict def d /k 1 put save /a 2 def /b 3 def d /k 2 put d /k undef 1 dict dup /z 9 put d copy pop \
|
||||
/a 4 store restore a 1 eq /b where not and d /k get 1 eq and d /z known not and",
|
||||
);
|
||||
super::tests::check("save dup restore { restore } stopped");
|
||||
super::tests::check("save save exch restore { restore } stopped");
|
||||
// The font cache's sizes are numbers programs divide by (Illustrator 8's pattern fills).
|
||||
super::tests::check("cachestatus 7 1 roll 6 { pop } repeat 0 gt");
|
||||
}
|
||||
|
||||
@@ -20,12 +20,17 @@ const DEFAULT_FONT: &str = "Helvetica";
|
||||
|
||||
/// A font's family and style from its PostScript name: `Helvetica-BoldOblique` → (`Helvetica`,
|
||||
/// `Bold Oblique`), `TimesNewRomanPS-BoldMT` → (`Times New Roman`, `Bold`), without a subset
|
||||
/// prefix (`ABCDEF+`).
|
||||
/// prefix (`ABCDEF+`) or a reencoded copy's suffix (`*1`).
|
||||
pub fn family_style(name: &str) -> (String, String) {
|
||||
let name = match name.split_once('+') {
|
||||
Some((prefix, rest)) if prefix.len() == 6 && prefix.bytes().all(|b| b.is_ascii_uppercase()) => rest,
|
||||
_ => name,
|
||||
};
|
||||
// A copy the program reencoded (`Impact*1`, as Illustrator's type procsets name them).
|
||||
let name = match name.rsplit_once('*') {
|
||||
Some((font, n)) if !font.is_empty() && !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit()) => font,
|
||||
_ => name,
|
||||
};
|
||||
let (family, style) = name.split_once('-').unwrap_or((name, ""));
|
||||
let trim = |s: &str| -> String {
|
||||
let s = ["PSMT", "MT", "PS"].iter().find_map(|suffix| s.strip_suffix(suffix)).unwrap_or(s);
|
||||
|
||||
Reference in New Issue
Block a user