.ai import: the -- placeholder in a gradient highlight and the XC operator no longer send the file to its PDF part (fixes #4176)

An undefined number (--) in `-- -- angle length Bh` is read as a radial
gradient with no highlight, and XC (a color space name after a linked
image's file name) is ignored like its neighbours. Either one used to make
the whole editing data unreadable, so the file opened as its PDF part and
lost its layers and live text. A -- anywhere else is still an operator the
reader doesn't know.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
YOU WISH! GAMES
2026-10-26 08:18:25 +07:00
committed by bflatastic
co-authored by Claude Sonnet 5.5
parent 0bd011bd18
commit 9af023b084
4 changed files with 63 additions and 12 deletions
+5 -2
View File
@@ -242,10 +242,13 @@ impl Instance {
self.xm = Self::matrix(vals);
}
/// `x y angle length Bh`.
/// `x y angle length Bh`. The point is `--` (undefined) when the gradient has no highlight.
pub fn bh(&mut self, vals: &[V]) {
let n: Vec<f64> = vals.iter().filter_map(V::num).collect();
if let [x, y, ..] = n.as_slice() {
if let [x, y, ..] = n.as_slice()
&& x.is_finite()
&& y.is_finite()
{
self.hilight = Vec2::new(*x, *y);
}
}
+25 -10
View File
@@ -562,6 +562,8 @@ const IGNORED: &[&str] = &[
"Xd",
"Xr",
"XG",
// `/DeviceGray XC` after a linked image's file name (`XG`): its colour space.
"XC",
"Xh",
"XH",
"XF",
@@ -1128,6 +1130,9 @@ impl<'a> Reader<'a> {
fn op(&mut self, w: &str, hidden: bool) -> Result<(), String> {
match w {
// The writer's placeholder for a number the object doesn't have (a radial gradient's
// highlight `-- -- angle length Bh`): a NaN keeps the other operands where they are.
"--" => return self.push(V::Num(f64::NAN)),
"[" => {
self.marks.push(self.stack.len());
return self.push(V::Mark);
@@ -1184,6 +1189,12 @@ impl<'a> Reader<'a> {
let group_ended = std::mem::take(&mut self.group_ended);
let vals = self.operands();
let vals = self.take_head(vals);
// Only `Bh` reads an undefined number. Anywhere else it would reach a point or a size, so
// the operator is one the reader doesn't know, as before.
if w != "Bh" && !self.frames.is_empty() && vals.iter().any(|v| matches!(v, V::Num(n) if n.is_nan())) {
self.note_unknown("--");
return Ok(());
}
if let Some(d) = self.gradient_def.as_mut() {
match w {
"Bs" => d.stop(&vals, &mut self.stop_spots),
@@ -1390,20 +1401,24 @@ impl<'a> Reader<'a> {
_ if IGNORED.contains(&w) => {}
// Before the art, the setup runs procedures of the printing format.
_ if self.frames.is_empty() => {}
_ => {
let op: String = w.chars().take(24).collect();
if !self.shown() {
if self.hidden_unread.len() < 16 {
self.hidden_unread.insert(format!("`{op}`"));
}
} else if self.unknown.len() < 16 {
self.unknown.insert(op);
}
}
_ => self.note_unknown(w),
}
Ok(())
}
/// An operator the reader doesn't know: on hidden art it is only noted, on shown art the file
/// is read from its PDF part.
fn note_unknown(&mut self, w: &str) {
let op: String = w.chars().take(24).collect();
if !self.shown() {
if self.hidden_unread.len() < 16 {
self.hidden_unread.insert(format!("`{op}`"));
}
} else if self.unknown.len() < 16 {
self.unknown.insert(op);
}
}
/// `:` after `/Type`: a dictionary opens.
fn begin_obj(&mut self, hidden: bool) -> Result<(), String> {
let ty = match self.stack.pop() {
+32
View File
@@ -1161,6 +1161,38 @@ fn ai_private_data() {
assert!(decode_private(b"%AI12_CompressedDataxx").is_err());
}
const G_DEFS: &str = "%AI5_BeginGradient: (G)\n(G) 1 2 Bd\n[\n<00FF>\n0\n0 0 0 0 1 0 0 2 1 6 %_BS\n\
%_0 0 0 0 1 0 0 2 1 6 40 0 Bs\n%_0 0 0 0 0 0 1 2 0.5 6 50 100 Bs\nBD\n%AI5_EndGradient\n";
/// A radial gradient instance whose highlight line is `bh`, on a rectangle.
fn highlighted(bh: &str) -> String {
let art_ = format!("{RED}{}Bb\n{bh} Bh\n1 (G) 0 0 0 1 1 0 0 1 0 0 1 Bg\n50 0 0 -50 100 50 Bm\nf\n0 BB\n", rect_path(50.0, 0.0, 150.0, 100.0));
format!("{G_DEFS}{}", layer(&art_))
}
#[test]
fn an_undefined_highlight_is_a_gradient_without_one() {
// `--` stands for a number the writer has no value for, here the highlight's point.
let none = read_ok(&highlighted("-- -- 269.6 0.87")).doc;
let zero = read_ok(&highlighted("0 0 269.6 0.87")).doc;
let Paint::Gradient(g) = fill(&art(&none)[0]) else { panic!("{:?}", fill(&art(&none)[0])) };
let Paint::Gradient(z) = fill(&art(&zero)[0]) else { panic!("{:?}", fill(&art(&zero)[0])) };
assert_eq!(g.gradient.kind, GradientKind::Radial);
assert_eq!(g.geom, z.geom, "the same gradient as one with the highlight at the centre");
}
#[test]
fn an_undefined_number_anywhere_else_is_still_an_operator_the_reader_doesnt_know() {
let err = read(stream(&layer(&format!("{RED}-- 5 m\n0 10 L\n10 10 L\nf\n"))).as_bytes()).unwrap_err();
assert!(err.contains("operators VectorCraft doesn't know (`--`)"), "{err}");
}
#[test]
fn a_linked_images_colour_space_operator_is_ignored() {
let art_ = format!("{RED}1 /DeviceGray XC\n{}f\n", rect_path(0.0, 0.0, 10.0, 10.0));
assert_eq!(art(&read_ok(&layer(&art_)).doc).len(), 1);
}
#[test]
fn many_zstandard_frames_are_read_in_one_pass_and_capped() {
let frame = zstd(b" ");
+1
View File
@@ -130,6 +130,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | minor | `.ai` import: the `--` placeholder of a radial gradient's highlight (`-- -- angle length Bh`, a gradient without one) and the `XC` operator after a linked image's file name no longer send the whole file to its PDF part, so files with them keep their layers, groups and live text (#4176). A `--` anywhere else is still an operator the reader doesn't know. Own-fixture tests in `import::ai::tests`; scores and hours unchanged |
| 2026-10-24 | minor | EPS, EMF/WMF and legacy `.ai`: type with ruby (ふりがな) is written with the reading's glyphs where the canvas draws them (they were dropped); legacy `.ai` writes such type as outlines, since a text object can't hold ruby. Tests `eps::tests::ruby_is_written_with_its_type`, `tests_legacy_ai::type_with_ruby_is_outlines_with_the_reading`, `metafile::tests::ruby_is_written_with_its_type`; scores and hours unchanged |
| 2026-10-24 | minor | `.ai` import: an image larger than the editing data's reader takes (2^25 pixels, a halftone texture at print resolution) no longer leaves its layer blank: the file comes in as its PDF part, which reads larger images, with a note saying why (#1479). Own-fixture test; scores and hours unchanged |
| 2026-10-24 | minor | PDF export: a pattern fill or stroke is written as a PDF tiling pattern (ISO 32000-1, 8.7.3.1) of one period of the tiling, as the canvas repeats it, so grid, brick and hex tilings and overlapping tiles keep their look in a few kilobytes, and PDF import reads it back as one object with a pattern swatch instead of hundreds of tile paths (#3849). Patterns nested more than four deep, or with tiles beyond a billion points, are still written as their tiles, now with a warning. Tests: the issue's hatch round trip, a brick tiling read back against the canvas, the nesting warning; scores and hours unchanged |