Clarify EPS import and interpreter stages for strict Clippy

This commit is contained in:
Dylan M. Taylor
2026-10-28 04:52:26 +00:00
parent e1bc6b235c
commit a9e82efcf0
22 changed files with 369 additions and 295 deletions
+4 -1
View File
@@ -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.
+1 -1
View File
@@ -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 ----------------------------------------------------------------------------
+1 -1
View File
@@ -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> {
+1 -1
View File
@@ -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<Unit> {
+46 -35
View File
@@ -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<V> = 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<V> = 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(()),
_ => {}
+1 -1
View File
@@ -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;
+4 -1
View File
@@ -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)]
+4 -1
View File
@@ -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;
+1 -1
View File
@@ -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 {
+96 -82
View File
@@ -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 <current space>] 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 <current space>] 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()?;
+5 -1
View File
@@ -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 ----------
+4 -1
View File
@@ -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.
+5 -1
View File
@@ -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)]
+137 -126
View File
@@ -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()))?;
+1 -1
View File
@@ -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)]
+1 -1
View File
@@ -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<Document> {
+3 -2
View File
@@ -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.
+41 -33
View File
@@ -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<Vec<(String, Point)>>], shown: &[Vec<Arc<Node>>]) -> Vec<Option<usize>> {
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<Rect>) {
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<Option<usize>> = 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<Content> = 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));
}
+1 -1
View File
@@ -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";
+4 -1
View File
@@ -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`,
+1 -1
View File
@@ -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,
+7 -1
View File
@@ -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,