From a9e82efcf0a7557f90066b0fe4be703d9aa55798 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 28 Oct 2026 04:52:26 +0000 Subject: [PATCH] Clarify EPS import and interpreter stages for strict Clippy --- crates/eps/src/import/ai/read/art.rs | 5 +- crates/eps/src/import/ai/read/containers.rs | 2 +- crates/eps/src/import/ai/read/directives.rs | 2 +- crates/eps/src/import/ai/read/document.rs | 2 +- crates/eps/src/import/ai/read/ops.rs | 81 +++--- crates/eps/src/import/ai/read/types.rs | 2 +- crates/eps/src/import/data/filters.rs | 5 +- crates/eps/src/import/data/ops.rs | 5 +- crates/eps/src/import/data/pixels.rs | 2 +- crates/eps/src/import/graphics/ops.rs | 178 +++++++------ crates/eps/src/import/graphics/paint.rs | 6 +- crates/eps/src/import/graphics/path.rs | 5 +- crates/eps/src/import/graphics/state.rs | 6 +- crates/eps/src/import/interp.rs | 263 ++++++++++---------- crates/eps/src/import/native/build.rs | 2 +- crates/eps/src/import/native/compare.rs | 2 +- crates/eps/src/import/native/layers.rs | 5 +- crates/eps/src/import/native/plan.rs | 74 +++--- crates/eps/src/import/native/slots.rs | 2 +- crates/eps/src/legacy_ai/encoding.rs | 5 +- crates/eps/src/legacy_ai/prolog.rs | 2 +- crates/eps/src/legacy_ai/writer.rs | 8 +- 22 files changed, 369 insertions(+), 295 deletions(-) diff --git a/crates/eps/src/import/ai/read/art.rs b/crates/eps/src/import/ai/read/art.rs index fcc2cb3c4..52bd7e685 100644 --- a/crates/eps/src/import/ai/read/art.rs +++ b/crates/eps/src/import/ai/read/art.rs @@ -1,7 +1,10 @@ use super::containers::close; use super::document::TEXT_SLOT; use super::types::{FLIP, IMAGE_TOO_LARGE, LINKED_PREVIEWS, MAX_PIXELS, MAX_POINTS, MAX_SIDE, NON_NATIVE_ART, NON_NATIVE_ART_UNREAD, Place}; -use super::*; +use super::{ + Affine, Appearance, AppearanceItem, Arc, BezPath, CANVAS_CENTRE, FillLayer, ImageBlob, ImageObject, Inks, Instance, LinkInfo, Node, NodeKind, + Obj, OpacityMask, Paint, PathData, Point, Reader, Rect, StrokeLayer, V, ps, +}; impl<'a> Reader<'a> { /// A text object (`/AI11Text`): an empty group standing for it, named after its story. diff --git a/crates/eps/src/import/ai/read/containers.rs b/crates/eps/src/import/ai/read/containers.rs index b1168b2bc..09ccd14aa 100644 --- a/crates/eps/src/import/ai/read/containers.rs +++ b/crates/eps/src/import/ai/read/containers.rs @@ -1,7 +1,7 @@ use super::document::slot_of; use super::ops::{paints_apart, path_rule}; use super::types::{Frame, GState, Kind, MAX_DEPTH, MAX_NODES}; -use super::*; +use super::{AppearanceItem, Arc, BezPath, BlendMode, FillRule, HashSet, LayerColor, Node, NodeId, NodeKind, Obj, Paint, PathEl, Rc, Reader, V, obj}; impl<'a> Reader<'a> { // ---- frames ---------------------------------------------------------------------------- diff --git a/crates/eps/src/import/ai/read/directives.rs b/crates/eps/src/import/ai/read/directives.rs index b3e7d6801..47eef88dd 100644 --- a/crates/eps/src/import/ai/read/directives.rs +++ b/crates/eps/src/import/ai/read/directives.rs @@ -1,6 +1,6 @@ use super::art::{four, sane, two}; use super::types::{FLIP, Kind, MAX_HEX, MAX_PATTERNS, MAX_STACK, PatternHead, Place, Raster, SKIPPED}; -use super::*; +use super::{Arc, Node, PatternDef, Reader, Rect, Tok, V, marker}; impl<'a> Reader<'a> { pub(super) fn push(&mut self, v: V) -> Result<(), String> { diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs index b61b74953..e78d91c82 100644 --- a/crates/eps/src/import/ai/read/document.rs +++ b/crates/eps/src/import/ai/read/document.rs @@ -1,7 +1,7 @@ use super::art::{element_name, elements, join_and, sane}; use super::containers::drop_commented_copies; use super::types::{FLIP, Kind, MAX_DEPTH}; -use super::*; +use super::{Affine, Artboard, ColorMode, Document, NodeKind, Obj, OpacityMask, Paint, Point, Reader, Rect, Structure, Swatch, Unit, V, obj}; /// `%AI5_RulerUnits` → [`Unit`] (Illustrator 7 spec table 3; centimeters is 4; pixels is 6 in later files). fn ruler_unit(v: u8) -> Option { diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index 24105cf28..9431061fe 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -1,11 +1,53 @@ use super::containers::close; use super::document::slot_of; use super::types::{BLENDS, FLIP, Frame, IGNORED, Kind, MAX_DEPTH}; -use super::*; +use super::{ + Affine, Appearance, AppearanceItem, Arc, BlendMode, Dash, FillLayer, FillRule, Gradient, GradientDef, HashMap, HashSet, Instance, Knockout, + LayerColor, LineCap, LineJoin, Node, NodeId, NodeKind, Paint, Reader, Rect, StrokeLayer, V, obj, paint, +}; impl<'a> Reader<'a> { // ---- operators ------------------------------------------------------------------------- + fn end_array(&mut self) -> Result<(), String> { + // The innermost `[` still on the stack (the stack is cut back by operators). + let mut at = self.stack.len(); + while let Some(m) = self.marks.pop() { + if matches!(self.stack.get(m), Some(V::Mark)) { + at = m; + break; + } + } + let items: Vec = self.stack.split_off(at).into_iter().skip(1).collect(); + self.push(V::Arr(items)) + } + + fn add_object_entry(&mut self) { + if let Some(Frame { kind: Kind::Obj(o), base, .. }) = self.frames.last_mut() { + obj::add_entry(o, &mut self.stack, *base); + // Binary data in ASCII85 follows, up to its `~>`: a `/Binary` dictionary's + // (`/ASCII85Decode ,`) or a non-native art object's (`/ForeignObject`'s + // `/Data ,`, the PDF it keeps), in comment lines. ASCII85 has `_`, so a line + // of it may start with `%_` and read as hidden tokens: a `(` there would open + // a string that swallows the rest of the layer. + let key = o.entries.last().and_then(|(k, _)| k.as_deref()); + if o.ty == "Binary" && key == Some("ASCII85Decode") { + self.lex.skip_past(b"~>"); + } else if o.ty == "ForeignObject" && key == Some("Data") { + self.foreign_data = Some(self.lex.skip_past(b"~>").to_vec()); + } + } + } + + fn open_mask_object(&mut self, hidden: bool) -> Result<(), String> { + if !matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(o), .. }) if o.ty == "Mask") { + // `X=` also introduces legacy text frame art, read by `text_frames`. + return Ok(()); + } + self.end_path_quietly(); + self.open(Kind::Mask, hidden) + } + pub(super) fn op(&mut self, w: &str, hidden: bool) -> Result<(), String> { if self.blend_record.is_some() { self.end_blend(); @@ -18,45 +60,14 @@ impl<'a> Reader<'a> { self.marks.push(self.stack.len()); return self.push(V::Mark); } - "]" => { - // The innermost `[` still on the stack (the stack is cut back by operators). - let mut at = self.stack.len(); - while let Some(m) = self.marks.pop() { - if matches!(self.stack.get(m), Some(V::Mark)) { - at = m; - break; - } - } - let items: Vec = self.stack.split_off(at).into_iter().skip(1).collect(); - return self.push(V::Arr(items)); - } + "]" => return self.end_array(), ":" => return self.begin_obj(hidden), "," => { - if let Some(Frame { kind: Kind::Obj(o), base, .. }) = self.frames.last_mut() { - obj::add_entry(o, &mut self.stack, *base); - // Binary data in ASCII85 follows, up to its `~>`: a `/Binary` dictionary's - // (`/ASCII85Decode ,`) or a non-native art object's (`/ForeignObject`'s - // `/Data ,`, the PDF it keeps), in comment lines. ASCII85 has `_`, so a line - // of it may start with `%_` and read as hidden tokens: a `(` there would open - // a string that swallows the rest of the layer. - let key = o.entries.last().and_then(|(k, _)| k.as_deref()); - if o.ty == "Binary" && key == Some("ASCII85Decode") { - self.lex.skip_past(b"~>"); - } else if o.ty == "ForeignObject" && key == Some("Data") { - self.foreign_data = Some(self.lex.skip_past(b"~>").to_vec()); - } - } + self.add_object_entry(); return Ok(()); } ";" => return self.end_obj(), - "X=" => { - if !matches!(self.frames.last(), Some(Frame { kind: Kind::Obj(o), .. }) if o.ty == "Mask") { - // `X=` also introduces legacy text frame art, read by `text_frames`. - return Ok(()); - } - self.end_path_quietly(); - return self.open(Kind::Mask, hidden); - } + "X=" => return self.open_mask_object(hidden), "X+" if self.frames.iter().any(|f| matches!(f.kind, Kind::Mask)) => return self.end_mask(), "X+" => return Ok(()), _ => {} diff --git a/crates/eps/src/import/ai/read/types.rs b/crates/eps/src/import/ai/read/types.rs index 4c15b04fa..309061f3c 100644 --- a/crates/eps/src/import/ai/read/types.rs +++ b/crates/eps/src/import/ai/read/types.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Affine, BlendMode, Dash, FillRule, Knockout, LayerColor, LineCap, LineJoin, Node, Obj, Paint, Rect, V}; /// Deepest nesting of layers, groups and dictionaries. pub(super) const MAX_DEPTH: usize = 256; diff --git a/crates/eps/src/import/data/filters.rs b/crates/eps/src/import/data/filters.rs index 088dd1b3a..d049e39a9 100644 --- a/crates/eps/src/import/data/filters.rs +++ b/crates/eps/src/import/data/filters.rs @@ -1,5 +1,8 @@ use super::ops::MAX_DATA; -use super::*; +use super::{ + DecodeSettings, Decoder, DecoderContext, DictRef, EncodingMode, ImageBlob, Key, Obj, PsError, Rc, Res, find, hex_decode, jpeg_components, ps, + ps_err, +}; /// A file object: the program itself, or data a filter decoded. #[derive(Debug)] diff --git a/crates/eps/src/import/data/ops.rs b/crates/eps/src/import/data/ops.rs index 63db3611f..3b4046de9 100644 --- a/crates/eps/src/import/data/ops.rs +++ b/crates/eps/src/import/data/ops.rs @@ -1,6 +1,9 @@ use super::filters::{Filter, Predictor, Stream, decode, fax_settings, io}; use super::pixels::{Pixels, Spec, by_rows, by_samples, masked, source}; -use super::*; +use super::{ + Affine, Color, DictRef, HashMap, ImageBlob, ImageObject, Inks, Interp, Key, Lexer, Node, NodeId, NodeKind, Obj, Op, OpacityMask, Paint, PsError, + Rc, RefCell, Res, Shared, Space, find, matrix_of, process, ps_err, +}; /// Most bytes one filter or image reads. pub(super) const MAX_DATA: usize = 1 << 28; diff --git a/crates/eps/src/import/data/pixels.rs b/crates/eps/src/import/data/pixels.rs index f61d10448..67ced02af 100644 --- a/crates/eps/src/import/data/pixels.rs +++ b/crates/eps/src/import/data/pixels.rs @@ -1,5 +1,5 @@ use super::ops::Source; -use super::*; +use super::{Affine, Obj, Res, Space, ps_err}; /// An image to decode. pub(super) struct Spec { diff --git a/crates/eps/src/import/graphics/ops.rs b/crates/eps/src/import/graphics/ops.rs index 608a62413..78a214b41 100644 --- a/crates/eps/src/import/graphics/ops.rs +++ b/crates/eps/src/import/graphics/ops.rs @@ -1,10 +1,100 @@ use super::paint::is_mesh; use super::state::{GState, Space}; -use super::*; +use super::{ + Affine, Color, FillRule, Interp, Key, LineCap, LineJoin, MAX_DASH_ENTRIES, MAX_DASHES, MAX_GSAVE, MAX_PATH, Obj, Op, Paint, Point, PsError, Rc, + Res, Shape, Vec2, matrix_obj, ps_err, +}; impl Interp<'_> { // ---------- the operators ---------- + fn replace_path_with_stroke_outline(&mut self) -> Res { + let bp = self.take_path(); + let mut st = self.stroke_layer()?; + // A dash pattern far finer than the path would make countless pieces. + let period: f64 = st.dash.as_ref().map_or(0.0, |d| d.pattern.iter().sum()); + let periods = if st.dash.is_some() && period > 0.0 { bp.perimeter(1e-3) / period } else { 0.0 }; + if st.dash.is_some() && !(period > 0.0 && periods < MAX_DASHES) { + st.dash = None; + } + let tol = (0.01 / self.scale().max(1e-9)).clamp(1e-4, 1.0); + let outline = vectorcraft_effects::stroke::line_outline(&bp, &st, st.width, tol); + // The path, each entry of the dash pattern in each period, and the outline count as elements. + let dashes = st.dash.as_ref().map_or(0.0, |d| periods * d.pattern.len() as f64) as usize; + self.spend_elements(bp.elements().len().saturating_add(dashes).saturating_add(outline.elements().len()))?; + if outline.elements().len() >= MAX_PATH { + return Err(PsError::Limit("a path has too many points")); + } + self.g.cur = outline.elements().iter().rev().find_map(|e| e.end_point()); + self.g.start = self.g.cur; + self.g.path = outline; + Ok(()) + } + + fn push_hsb_color(&mut self) -> Res { + let [r, g, b] = self.current_rgb(); + let (max, min) = (r.max(g).max(b), r.min(g).min(b)); + let d = max - min; + let h = if d <= 0.0 { + 0.0 + } else if max == r { + ((g - b) / d).rem_euclid(6.0) / 6.0 + } else if max == g { + ((b - r) / d + 2.0) / 6.0 + } else { + ((r - g) / d + 4.0) / 6.0 + }; + let s = if max > 0.0 { d / max } else { 0.0 }; + for v in [h, s, max] { + self.push_num(f64::from(v))?; + } + Ok(()) + } + + fn set_current_color(&mut self) -> Res { + if let Space::Pattern(base) = &*self.g.space { + self.set_pattern(base.clone())?; + } else { + let mut components = vec![0.0; self.g.space.n()]; + for slot in components.iter_mut().rev() { + *slot = self.pop_num()?; + } + self.set_comps(components)?; + } + Ok(()) + } + + fn transform_point(&mut self, op: Op) -> Res { + let m = match self.stack.last() { + Some(Obj::Array { .. }) => self.pop_matrix()?, + _ => self.g.ctm, + }; + let [x, y] = self.nums()?; + let inverse = matches!(op, Op::ITransform | Op::IDTransform); + if inverse && m.determinant().abs() < 1e-12 { + return ps_err("undefinedresult", ""); + } + let m = if inverse { m.inverse() } else { m }; + let p = if matches!(op, Op::DTransform | Op::IDTransform) { + let v = m * Point::new(x, y) - m * Point::ZERO; + Point::new(v.x, v.y) + } else { + m * Point::new(x, y) + }; + self.push_num(p.x)?; + self.push_num(p.y) + } + + // `[/Pattern ] setcolorspace setcolor`, unless already in a pattern space. + fn set_pattern_color(&mut self) -> Res { + if !matches!(*self.g.space, Space::Pattern(_)) { + self.g.space = Rc::new(Space::Pattern(Some(self.g.space.clone()))); + self.g.space_obj = Obj::array(vec![Obj::name("Pattern"), self.g.space_obj.clone()]); + } + let Space::Pattern(base) = &*self.g.space else { return ps_err("typecheck", "") }; + self.set_pattern(base.clone()) + } + pub fn graphics_op(&mut self, op: Op) -> Res { use Op::*; match op { @@ -26,28 +116,7 @@ impl Interp<'_> { self.g.clips.clear(); } ExecForm => self.exec_form()?, - StrokePath => { - let bp = self.take_path(); - let mut st = self.stroke_layer()?; - // A dash pattern far finer than the path would make countless pieces. - let period: f64 = st.dash.as_ref().map_or(0.0, |d| d.pattern.iter().sum()); - let periods = if st.dash.is_some() && period > 0.0 { bp.perimeter(1e-3) / period } else { 0.0 }; - if st.dash.is_some() && !(period > 0.0 && periods < MAX_DASHES) { - st.dash = None; - } - let tol = (0.01 / self.scale().max(1e-9)).clamp(1e-4, 1.0); - let outline = vectorcraft_effects::stroke::line_outline(&bp, &st, st.width, tol); - // The path, each entry of the dash pattern in each period, and the outline count - // as elements. - let dashes = st.dash.as_ref().map_or(0.0, |d| periods * d.pattern.len() as f64) as usize; - self.spend_elements(bp.elements().len().saturating_add(dashes).saturating_add(outline.elements().len()))?; - if outline.elements().len() >= MAX_PATH { - return Err(PsError::Limit("a path has too many points")); - } - self.g.cur = outline.elements().iter().rev().find_map(|e| e.end_point()); - self.g.start = self.g.cur; - self.g.path = outline; - } + StrokePath => self.replace_path_with_stroke_outline()?, PathForAll => self.path_for_all()?, UFill | UEoFill | UStroke => self.paint_user_path(op)?, UAppend => { @@ -58,24 +127,7 @@ impl Interp<'_> { SetBBox => { self.nums::<4>()?; } - CurrentHsbColor => { - let [r, g, b] = self.current_rgb(); - let (max, min) = (r.max(g).max(b), r.min(g).min(b)); - let d = max - min; - let h = if d <= 0.0 { - 0.0 - } else if max == r { - ((g - b) / d).rem_euclid(6.0) / 6.0 - } else if max == g { - ((b - r) / d + 2.0) / 6.0 - } else { - ((r - g) / d + 4.0) / 6.0 - }; - let s = if max > 0.0 { d / max } else { 0.0 }; - for v in [h, s, max] { - self.push_num(f64::from(v))?; - } - } + CurrentHsbColor => self.push_hsb_color()?, CurrentColorRendering => { let d = crate::import::obj::Dict::from([(Key::name("ColorRenderingType"), Obj::Int(1))]); self.push(Obj::dict(d))?; @@ -201,32 +253,13 @@ impl Interp<'_> { }; self.push(o)?; } - SetColor => { - if let Space::Pattern(base) = &*self.g.space { - self.set_pattern(base.clone())?; - } else { - let n = self.g.space.n(); - let mut c = vec![0.0; n]; - for slot in c.iter_mut().rev() { - *slot = self.pop_num()?; - } - self.set_comps(c)?; - } - } + SetColor => self.set_current_color()?, CurrentColor => { for v in self.g.comps.clone() { self.push_num(v)?; } } - // `[/Pattern ] setcolorspace setcolor`, unless in a pattern space already. - SetPattern => { - if !matches!(*self.g.space, Space::Pattern(_)) { - self.g.space = Rc::new(Space::Pattern(Some(self.g.space.clone()))); - self.g.space_obj = Obj::array(vec![Obj::name("Pattern"), self.g.space_obj.clone()]); - } - let Space::Pattern(base) = &*self.g.space else { return ps_err("typecheck", "") }; - self.set_pattern(base.clone())?; - } + SetPattern => self.set_pattern_color()?, MakePattern => { let m = self.pop_matrix()?; let d = self.pop_dict()?; @@ -347,26 +380,7 @@ impl Interp<'_> { None => self.g.ctm *= m, } } - Transform | ITransform | DTransform | IDTransform => { - let m = match self.stack.last() { - Some(Obj::Array { .. }) => self.pop_matrix()?, - _ => self.g.ctm, - }; - let [x, y] = self.nums()?; - let inverse = matches!(op, ITransform | IDTransform); - if inverse && m.determinant().abs() < 1e-12 { - return ps_err("undefinedresult", ""); - } - let m = if inverse { m.inverse() } else { m }; - let p = if matches!(op, DTransform | IDTransform) { - let v = m * Point::new(x, y) - m * Point::ZERO; - Point::new(v.x, v.y) - } else { - m * Point::new(x, y) - }; - self.push_num(p.x)?; - self.push_num(p.y)?; - } + Transform | ITransform | DTransform | IDTransform => self.transform_point(op)?, InvertMatrix => { let out = self.pop_array()?; let m = self.pop_matrix()?; diff --git a/crates/eps/src/import/graphics/paint.rs b/crates/eps/src/import/graphics/paint.rs index f2e343858..8b0a729ee 100644 --- a/crates/eps/src/import/graphics/paint.rs +++ b/crates/eps/src/import/graphics/paint.rs @@ -1,5 +1,9 @@ use super::state::{GState, Space, process, tiling_hash}; -use super::*; +use super::{ + Affine, Arc, Color, DictRef, FUNCTION_OPS, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, HashSet, Interp, Key, LIMIT, + MAX_APART, MAX_FUNCTION_VISITS, MAX_HIVAL, MAX_INKS, MAX_STITCH_STOPS, NESTED_PATTERNS, Node, NodeId, NodeKind, Obj, Paint, PatternDef, Point, + PsError, REGISTRATION, Rc, Rect, Res, SHADING_SAMPLES, Shared, UNKNOWN_FUNCTIONS, UNKNOWN_SHADINGS, Vec2, clamp_between, ps_err, +}; impl Interp<'_> { // ---------- colour ---------- diff --git a/crates/eps/src/import/graphics/path.rs b/crates/eps/src/import/graphics/path.rs index 3403e9add..f9580077e 100644 --- a/crates/eps/src/import/graphics/path.rs +++ b/crates/eps/src/import/graphics/path.rs @@ -1,5 +1,8 @@ use super::state::{ccw_turn, turn}; -use super::*; +use super::{ + Affine, BezPath, Dash, FillRule, HAIRLINE, Interp, Key, MAX_GSAVE, MAX_NEST, MAX_PATH, Obj, Op, PathEl, Point, PsError, Rect, Res, Shape, + StrokeLayer, Vec2, deep_clips, ps_err, +}; impl Interp<'_> { /// User space → document space. diff --git a/crates/eps/src/import/graphics/state.rs b/crates/eps/src/import/graphics/state.rs index 6aa25d5a9..489f675bf 100644 --- a/crates/eps/src/import/graphics/state.rs +++ b/crates/eps/src/import/graphics/state.rs @@ -1,4 +1,8 @@ -use super::*; +use super::{ + Affine, Appearance, AppearanceItem, Arc, BezPath, Clip, Color, DefaultHasher, DictRef, Document, Drawn, FAR_AWAY, FillLayer, FillRule, Hash, + HashMap, HashSet, Hasher, LIMIT, LineCap, LineJoin, MAX_DRAWN, MAX_NEST, MAX_NODES, Node, NodeId, NodeKind, Obj, Paint, PathData, Point, Rc, + Rect, Shared, StrokeLayer, TOO_MUCH, +}; /// A colour space. #[derive(Clone, Debug)] diff --git a/crates/eps/src/import/interp.rs b/crates/eps/src/import/interp.rs index 744431fad..5e8d12661 100644 --- a/crates/eps/src/import/interp.rs +++ b/crates/eps/src/import/interp.rs @@ -502,6 +502,138 @@ impl<'a> Interp<'a> { }) } + fn arithmetic(&mut self, op: Op) -> Res { + let (b, a) = (self.pop()?, self.pop()?); + let integer = match (&a, &b) { + (Obj::Int(x), Obj::Int(y)) => match op { + Op::Add => x.checked_add(*y).map(Obj::Int), + Op::Sub => x.checked_sub(*y).map(Obj::Int), + _ => x.checked_mul(*y).map(Obj::Int), + }, + _ => None, + }; + let result = match integer { + Some(value) => value, + None => { + let (x, y) = (a.as_num().ok_or(PsError::Ps("typecheck", String::new()))?, b.as_num().ok_or(PsError::Ps("typecheck", String::new()))?); + let value = match op { + Op::Add => x + y, + Op::Sub => x - y, + _ => x * y, + }; + if !value.is_finite() { + return ps_err("undefinedresult", ""); + } + Obj::Real(value) + } + }; + self.push(result) + } + + fn math_function(&mut self, op: Op) -> Res { + let result = match op { + Op::Sqrt => self.pop_num()?.sqrt(), + Op::Sin => self.pop_num()?.to_radians().sin(), + Op::Cos => self.pop_num()?.to_radians().cos(), + Op::Ln => self.pop_num()?.ln(), + Op::Log => self.pop_num()?.log10(), + Op::Exp => { + let [base, exponent] = self.nums()?; + base.powf(exponent) + } + _ => { + let [num, den] = self.nums()?; + num.atan2(den).to_degrees().rem_euclid(360.0) + } + }; + if !result.is_finite() { + return ps_err("undefinedresult", ""); + } + self.push(Obj::Real(result)) + } + + fn logical_op(&mut self, op: Op) -> Res { + let (b, a) = (self.pop()?, self.pop()?); + let result = match (a, b) { + (Obj::Bool(x), Obj::Bool(y)) => Obj::Bool(match op { + Op::And => x && y, + Op::Or => x || y, + _ => x ^ y, + }), + (Obj::Int(x), Obj::Int(y)) => Obj::Int(match op { + Op::And => x & y, + Op::Or => x | y, + _ => x ^ y, + }), + _ => return ps_err("typecheck", ""), + }; + self.push(result) + } + + fn for_loop(&mut self) -> Res { + let p = self.pop_proc()?; + let (limit, inc, init) = (self.pop()?, self.pop()?, self.pop()?); + match (&init, &inc, &limit) { + (Obj::Int(i0), Obj::Int(d), Obj::Int(l)) => { + let (mut i, d, l) = (*i0, *d, *l); + while (d >= 0 && i <= l) || (d < 0 && i >= l) { + self.push(Obj::Int(i))?; + if !self.body(&p)? { + break; + } + i = i.checked_add(d).ok_or(PsError::Ps("rangecheck", String::new()))?; + if d == 0 { + self.tick()?; + } + } + } + _ => { + let n = |o: &Obj| o.as_num().filter(|v| v.is_finite()).ok_or(PsError::Ps("typecheck", String::new())); + let (i0, d, l) = (n(&init)?, n(&inc)?, n(&limit)?); + let mut k = 0.0; + loop { + let i = i0 + d * k; + if !((d >= 0.0 && i <= l) || (d < 0.0 && i >= l)) { + break; + } + self.push(Obj::Real(i))?; + if !self.body(&p)? { + break; + } + k += 1.0; + } + } + } + Ok(()) + } + + fn search_string(&mut self, op: Op) -> Res { + let seek = self.pop_str()?.to_vec(); + let s = self.pop_str()?; + let text = s.to_vec(); + self.spend((text.len() / 64) as u64)?; + let at = if op == Op::Search { super::lex::find(&text, &seek) } else { text.starts_with(&seek).then_some(0) }; + match at { + Some(i) => { + let o = Obj::Str(s); + if op == Op::Search { + self.push_interval(o.clone(), i + seek.len(), text.len() - i - seek.len())?; + self.push_interval(o.clone(), i, seek.len())?; + self.push_interval(o, 0, i)?; + } else { + self.push_interval(o.clone(), seek.len(), text.len() - seek.len())?; + self.push_interval(o, 0, seek.len())?; + } + self.push(Obj::Bool(true))?; + } + None => { + self.push(Obj::Str(s))?; + self.push(Obj::Bool(false))?; + } + } + Ok(()) + } + fn op_inner(&mut self, op: Op) -> Res { use Op::*; match op { @@ -558,34 +690,7 @@ impl<'a> Interp<'a> { let n = self.stack.iter().rev().position(|o| matches!(o, Obj::Mark)).ok_or(PsError::Ps("unmatchedmark", String::new()))?; self.push(Obj::Int(n as i64))?; } - Add | Sub | Mul => { - let (b, a) = (self.pop()?, self.pop()?); - let r = match (&a, &b) { - (Obj::Int(x), Obj::Int(y)) => match op { - Add => x.checked_add(*y).map(Obj::Int), - Sub => x.checked_sub(*y).map(Obj::Int), - _ => x.checked_mul(*y).map(Obj::Int), - }, - _ => None, - }; - let r = match r { - Some(r) => r, - None => { - let (x, y) = - (a.as_num().ok_or(PsError::Ps("typecheck", String::new()))?, b.as_num().ok_or(PsError::Ps("typecheck", String::new()))?); - let r = match op { - Add => x + y, - Sub => x - y, - _ => x * y, - }; - if !r.is_finite() { - return ps_err("undefinedresult", ""); - } - Obj::Real(r) - } - }; - self.push(r)?; - } + Add | Sub | Mul => self.arithmetic(op)?, Div => { let [a, b] = self.nums()?; if b == 0.0 { @@ -603,27 +708,7 @@ impl<'a> Interp<'a> { Obj::Real(r) => self.push(Obj::Real(if op == Neg { -r } else { r.abs() }))?, _ => return ps_err("typecheck", ""), }, - Sqrt | Sin | Cos | Exp | Ln | Log | Atan => { - let r = match op { - Sqrt => self.pop_num()?.sqrt(), - Sin => self.pop_num()?.to_radians().sin(), - Cos => self.pop_num()?.to_radians().cos(), - Ln => self.pop_num()?.ln(), - Log => self.pop_num()?.log10(), - Exp => { - let [b, e] = self.nums()?; - b.powf(e) - } - _ => { - let [num, den] = self.nums()?; - num.atan2(den).to_degrees().rem_euclid(360.0) - } - }; - if !r.is_finite() { - return ps_err("undefinedresult", ""); - } - self.push(Obj::Real(r))?; - } + Sqrt | Sin | Cos | Exp | Ln | Log | Atan => self.math_function(op)?, Round | Floor | Ceiling | Truncate => match self.pop()? { Obj::Int(i) => self.push(Obj::Int(i))?, Obj::Real(r) => self.push(Obj::Real(match op { @@ -678,23 +763,7 @@ impl<'a> Interp<'a> { }; self.push(Obj::Bool(r))?; } - And | Or | Xor => { - let (b, a) = (self.pop()?, self.pop()?); - let r = match (a, b) { - (Obj::Bool(x), Obj::Bool(y)) => Obj::Bool(match op { - And => x && y, - Or => x || y, - _ => x ^ y, - }), - (Obj::Int(x), Obj::Int(y)) => Obj::Int(match op { - And => x & y, - Or => x | y, - _ => x ^ y, - }), - _ => return ps_err("typecheck", ""), - }; - self.push(r)?; - } + And | Or | Xor => self.logical_op(op)?, Not => match self.pop()? { Obj::Bool(b) => self.push(Obj::Bool(!b))?, Obj::Int(i) => self.push(Obj::Int(!i))?, @@ -723,41 +792,7 @@ impl<'a> Interp<'a> { let pick = if self.pop_bool()? { yes } else { no }; self.call(pick)?; } - For => { - let p = self.pop_proc()?; - let (limit, inc, init) = (self.pop()?, self.pop()?, self.pop()?); - match (&init, &inc, &limit) { - (Obj::Int(i0), Obj::Int(d), Obj::Int(l)) => { - let (mut i, d, l) = (*i0, *d, *l); - while (d >= 0 && i <= l) || (d < 0 && i >= l) { - self.push(Obj::Int(i))?; - if !self.body(&p)? { - break; - } - i = i.checked_add(d).ok_or(PsError::Ps("rangecheck", String::new()))?; - if d == 0 { - self.tick()?; - } - } - } - _ => { - let n = |o: &Obj| o.as_num().filter(|v| v.is_finite()).ok_or(PsError::Ps("typecheck", String::new())); - let (i0, d, l) = (n(&init)?, n(&inc)?, n(&limit)?); - let mut k = 0.0; - loop { - let i = i0 + d * k; - if !((d >= 0.0 && i <= l) || (d < 0.0 && i >= l)) { - break; - } - self.push(Obj::Real(i))?; - if !self.body(&p)? { - break; - } - k += 1.0; - } - } - } - } + For => self.for_loop()?, Repeat => { let p = self.pop_proc()?; let n = self.pop_int()?; @@ -955,31 +990,7 @@ impl<'a> Interp<'a> { self.stack.truncate(at); self.push(Obj::Array { items, exec: false })?; } - Search | AnchorSearch => { - let seek = self.pop_str()?.to_vec(); - let s = self.pop_str()?; - let text = s.to_vec(); - self.spend((text.len() / 64) as u64)?; - let at = if op == Search { super::lex::find(&text, &seek) } else { text.starts_with(&seek).then_some(0) }; - match at { - Some(i) => { - let o = Obj::Str(s); - if op == Search { - self.push_interval(o.clone(), i + seek.len(), text.len() - i - seek.len())?; - self.push_interval(o.clone(), i, seek.len())?; - self.push_interval(o, 0, i)?; - } else { - self.push_interval(o.clone(), seek.len(), text.len() - seek.len())?; - self.push_interval(o, 0, seek.len())?; - } - self.push(Obj::Bool(true))?; - } - None => { - self.push(Obj::Str(s))?; - self.push(Obj::Bool(false))?; - } - } - } + Search | AnchorSearch => self.search_string(op)?, NewDict => { self.pop_count()?; self.push(Obj::dict(Default::default()))?; diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index 9441da025..995e8e8d3 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -4,7 +4,7 @@ use super::slots::{ ART_LEFT_OUT, Content, MAX_PAIRS, MAX_SLOTS, MAX_TEXT, OUTLINE_REACH, TEXT_LEFT_OUT, TYPE_LEFT_OUT, collect_slots, extent, few, fill_slots, text_objects, }; -use super::*; +use super::{Affine, Arc, BTreeSet, Document, Node, NodeId, NodeKind, PathData, Rect, Scaling, Story, Structure, Texts, Unit, Vec2, ai}; /// Which box of the editing copy is the page. #[derive(Clone, Copy)] diff --git a/crates/eps/src/import/native/compare.rs b/crates/eps/src/import/native/compare.rs index ceaa584e2..cf86445e5 100644 --- a/crates/eps/src/import/native/compare.rs +++ b/crates/eps/src/import/native/compare.rs @@ -1,6 +1,6 @@ use super::build::Frame; use super::slots::OUTLINE_REACH; -use super::*; +use super::{Arc, Document, Node, NodeKind, Rect}; /// `doc` with its layers that don't print hidden, when it has any. fn as_printed(doc: &Document) -> Option { diff --git a/crates/eps/src/import/native/layers.rs b/crates/eps/src/import/native/layers.rs index e0e7eacc0..4566afe0d 100644 --- a/crates/eps/src/import/native/layers.rs +++ b/crates/eps/src/import/native/layers.rs @@ -1,6 +1,6 @@ use super::build::{Page, build}; use super::slots::LAYERS_UNREAD; -use super::*; +use super::{Affine, Arc, Cow, Document, Imported, Node, NodeKind, PathData, Rect, Vec2, ai}; /// The EPS `visible` (read as its page) read through its editing copy instead (its private data, or /// its program in the legacy format, see [`editing_copy`]): its layers (the @@ -217,7 +217,8 @@ pub(super) fn closed_where_ends_meet(mut path: PathData) -> PathData { #[cfg(test)] mod closed_ends_tests { - use super::*; + use super::{PathData, closed_where_ends_meet}; + use vectorcraft_geom::Point; /// A path drawn round to its start (the last anchor on the first) is closed there, its first /// anchor taking the last one's incoming handle; one whose ends are apart stays open. diff --git a/crates/eps/src/import/native/plan.rs b/crates/eps/src/import/native/plan.rs index aa86d57b6..a89ad0dd9 100644 --- a/crates/eps/src/import/native/plan.rs +++ b/crates/eps/src/import/native/plan.rs @@ -1,6 +1,44 @@ use super::layers::closed_where_ends_meet; use super::slots::{Content, SlotInfo, assign, covered, extent, join_pieces, letters, same_type}; -use super::*; +use super::{Affine, Arc, Node, NodeId, NodeKind, Point, Rect, Story, TextKind}; + +/// Match page objects to the nearest slot whose story describes them, including hidden slots. +fn match_page_objects(infos: &[SlotInfo], lines: &[Option>], shown: &[Vec>]) -> Vec> { + let mut pairs: Vec<(f64, usize, usize)> = vec![]; + for (k, _) in infos.iter().enumerate() { + let Some(l) = lines.get(k).and_then(Option::as_ref) else { continue }; + for (j, object) in shown.iter().enumerate() { + if let Some(d) = object.iter().filter_map(|n| same_type(n, l)).min_by(f64::total_cmp) { + pairs.push((d, k, j)); + } + } + } + pairs.sort_by(|a, b| a.0.total_cmp(&b.0)); + let mut owner = vec![None; shown.len()]; + for (_, k, j) in pairs { + if let Some(o) = owner.get_mut(j).filter(|o| o.is_none()) { + *o = Some(k); + } + } + owner +} + +fn frame_text_from_art(info: &SlotInfo, node: &mut Node, region: &mut Option) { + if let (Some(path), NodeKind::Text(text)) = (&info.art_path, &mut node.kind) { + let size = text.runs.iter().map(|r| r.style.size).fold(0.0, f64::max); + match &mut text.kind { + TextKind::Area { frame } => { + *frame = path.clone(); + *region = path.bounds(); + } + TextKind::OnPath { path: on, .. } => { + *on = closed_where_ends_meet(path.clone()); + *region = path.bounds().map(|r| r.inflate(size * 1.5, size * 1.5)); + } + TextKind::Point => {} + } + } +} /// What goes into each of `infos`' slots: the page's type that the file's text document puts where the /// page has it, else type made from the text document, else (for a file whose text document can't say) @@ -21,24 +59,7 @@ pub(super) fn plan( Some(s.line_starts(t).into_iter().map(|(text, (x, y))| (text, to_doc * Point::new(x, y))).collect()) }) .collect(); - // The page's objects that are where a slot's lines are, nearest first. A slot that doesn't show - // has them too: a file's page may draw what its layers have hidden since. - let mut pairs: Vec<(f64, usize, usize)> = vec![]; - for (k, _) in infos.iter().enumerate() { - let Some(l) = lines.get(k).and_then(Option::as_ref) else { continue }; - for (j, object) in shown.iter().enumerate() { - if let Some(d) = object.iter().filter_map(|n| same_type(n, l)).min_by(f64::total_cmp) { - pairs.push((d, k, j)); - } - } - } - pairs.sort_by(|a, b| a.0.total_cmp(&b.0)); - let mut owner: Vec> = vec![None; shown.len()]; - for (_, k, j) in pairs { - if let Some(o) = owner.get_mut(j).filter(|o| o.is_none()) { - *o = Some(k); - } - } + let owner = match_page_objects(infos, &lines, shown); // Type the text document makes, shown or not: the page's pieces of it aren't needed. let mut content: Vec = infos.iter().map(|_| Content::Empty).collect(); // Where the made type that doesn't show lies, by slot. @@ -51,20 +72,7 @@ pub(super) fn plan( let mut region = story.region(t, to_doc); // Area type and type on a path in the object's own frame: the text document's frame // of a copied object keeps its original's path. - if let (Some(path), NodeKind::Text(text)) = (&info.art_path, &mut node.kind) { - let size = text.runs.iter().map(|r| r.style.size).fold(0.0, f64::max); - match &mut text.kind { - TextKind::Area { frame } => { - *frame = path.clone(); - region = path.bounds(); - } - TextKind::OnPath { path: on, .. } => { - *on = closed_where_ends_meet(path.clone()); - region = path.bounds().map(|r| r.inflate(size * 1.5, size * 1.5)); - } - TextKind::Point => {} - } - } + frame_text_from_art(info, &mut node, &mut region); if let (false, Some(r)) = (info.shown, region) { hidden_at.push((k, r)); } diff --git a/crates/eps/src/import/native/slots.rs b/crates/eps/src/import/native/slots.rs index 07f7ef13d..524fbcc81 100644 --- a/crates/eps/src/import/native/slots.rs +++ b/crates/eps/src/import/native/slots.rs @@ -1,5 +1,5 @@ use super::build::MADE; -use super::*; +use super::{AppearanceItem, Arc, BTreeSet, BlendMode, Document, Node, NodeId, NodeKind, PathData, Point, Rect, ai, slot_of}; /// The notes that say the import left out something the file has (see [`is_loss`]). pub(super) const TEXT_LEFT_OUT: &str = "text objects on hidden layers or hidden objects couldn't be read, so they are left out"; diff --git a/crates/eps/src/legacy_ai/encoding.rs b/crates/eps/src/legacy_ai/encoding.rs index b67296e72..afa6a9b35 100644 --- a/crates/eps/src/legacy_ai/encoding.rs +++ b/crates/eps/src/legacy_ai/encoding.rs @@ -1,5 +1,8 @@ use super::writer::Writer; -use super::*; +use super::{ + Affine, AreaOptions, BezPath, CharPosition, CharStyle, Color, Gradient, GradientKind, Paint, ParaStyle, PathEffect, StrokeLayer, TextKind, + TextObject, Write, latin, ps, stroke, +}; /// A gradient definition: `%AI5_BeginGradient: (name)`, `(name) type stops Bd`, `[`, the stops /// (`colorSpec colorStyle midPoint rampPoint %_Bs`, each with its midpoint to the next stop), `BD`, diff --git a/crates/eps/src/legacy_ai/prolog.rs b/crates/eps/src/legacy_ai/prolog.rs index eb10f1440..554447cec 100644 --- a/crates/eps/src/legacy_ai/prolog.rs +++ b/crates/eps/src/legacy_ai/prolog.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Write, latin}; /// The setup's fonts (§3.1, §3.3): `%%IncludeFont` for each, the encoding strings are in (`TE`), /// and each font re-encoded to it as `_name` (`TZ`; a TrueType font takes the encoding as its own, diff --git a/crates/eps/src/legacy_ai/writer.rs b/crates/eps/src/legacy_ai/writer.rs index 31caf715f..89df7701b 100644 --- a/crates/eps/src/legacy_ai/writer.rs +++ b/crates/eps/src/legacy_ai/writer.rs @@ -1,7 +1,13 @@ use super::encoding::{ Ink, average, direct_fill, direct_stroke, finite, font_of, invertible, latin_string, lost_attributes, segment_point, unit, writable, }; -use super::*; +use super::{ + Affine, AnchorKind, AppearanceItem, Arc, BezPath, BlendMode, CharStyle, Cms, Color, Document, ELLIPTICAL, FREEFORM, FillLayer, FillRule, + Gradient, GradientKind, GradientPaint, HEX_LINE, HIDDEN, HIDDEN_LAYER, HIDDEN_SHOWN, HiddenObjects, IMAGE_ALPHA, ImageBlob, Justify, LARGE_IMAGE, + LAYER_COLORS, LAYER_NAME, LIVE, LayerColor, LegacyAiOptions, LineCap, LineJoin, MAX_IMAGE_PIXELS, MAX_NEST, NAMES, Node, NodeKind, PATTERNS, + Paint, ParaDirection, ParaStyle, PathData, Point, Rect, STROKES, SUBLAYERS, Shape, StrokeLayer, SubPath, TITLE_AND_COLOR_NAMES, TRANSPARENCY, + TYPE, TYPE_ATTRIBUTES, TextKind, TextObject, UNDECODED, Vec2, Write, latin, ps, stroke, +}; pub(super) struct Writer<'a> { doc: &'a Document,