Merge pull request #4179 from youwishgames/fix/ai-undefined-number-and-xc

.ai import: the -- placeholder in a gradient highlight and the XC operator no longer send the file to its PDF part (fixes #4176)
This commit is contained in:
BFlat
2026-10-26 08:41:09 +07:00
committed by GitHub
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 |