mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Merge pull request #6220 from dotCooCoo/fix/ai-writer-object-limit-2
.ai save: the editing copy stops at the reader's object limit
This commit is contained in:
@@ -169,7 +169,8 @@
|
||||
//! layers and opacity masks nested too deep, and, on objects written without an art style, live
|
||||
//! effects other than geometry effects left out; graphic styles the reader wouldn't read, links to
|
||||
//! graphic styles on objects not written with them, and every art style on a large canvas, are left
|
||||
//! out.
|
||||
//! out; art past [`crate::import::MAX_EDITING_OBJECTS`] objects in all (the most the reader reads),
|
||||
//! counted as the reader counts them, is left out.
|
||||
//!
|
||||
//! The data streams hold the program compressed with Zstandard after `%AI24_ZStandard_Data`, with
|
||||
//! the version numbers of the newest files users sent ([`private_data`]).
|
||||
@@ -242,6 +243,11 @@ pub struct AiEditingOutput {
|
||||
|
||||
/// Write `doc` as the editing data of a `.ai` file: every artboard and layer.
|
||||
pub fn export_ai_editing(doc: &Document, o: &AiEditingOptions) -> Result<AiEditingOutput, String> {
|
||||
export_within(doc, o, crate::import::MAX_EDITING_OBJECTS)
|
||||
}
|
||||
|
||||
/// [`export_ai_editing`] writing at most `max_objects` objects, counted as the reader counts them.
|
||||
fn export_within(doc: &Document, o: &AiEditingOptions, max_objects: usize) -> Result<AiEditingOutput, String> {
|
||||
let board = doc.artboards.first().ok_or("the document has no artboard")?.rect;
|
||||
if !(board.is_finite() && board.width() > 0.0 && board.height() > 0.0) {
|
||||
return Err("the document has no artboard".into());
|
||||
@@ -283,6 +289,7 @@ pub fn export_ai_editing(doc: &Document, o: &AiEditingOptions) -> Result<AiEditi
|
||||
// On a large canvas the layers' symbol instances are written as their art.
|
||||
w.symbols_live = scale == 1.0;
|
||||
w.symbol_doc = symbol_doc;
|
||||
w.max_objects = max_objects;
|
||||
w.reserve_swatch_gradients();
|
||||
if scale == 1.0 {
|
||||
w.styled = styled;
|
||||
|
||||
@@ -833,6 +833,178 @@ fn nested_symbol_instances_stop_at_the_symbol_art_limit() {
|
||||
assert_eq!(layers.matches("\r\nf\r\n").count(), 3198, "the squares of the leaves written");
|
||||
}
|
||||
|
||||
/// The documents [`the_writer_counts_objects_as_the_reader_reads_them`] writes: groups, compound
|
||||
/// paths, guides, sublayers, clipping groups and layers, painted clipping paths, opacity masks,
|
||||
/// gradients, pattern definitions and tiles, images, type, symbol definitions and instances,
|
||||
/// objects with art styles and objects bound to variables.
|
||||
fn counted() -> Vec<(&'static str, Document)> {
|
||||
vec![
|
||||
("sample", sample()),
|
||||
("unsupported", unsupported()),
|
||||
("transparent", transparent()),
|
||||
("gradients and patterns", gradients_and_patterns()),
|
||||
("images", images_doc()),
|
||||
("typed", typed()),
|
||||
("variables", variables_doc()),
|
||||
("symbols", symbols_document()),
|
||||
("styles", styles_document()),
|
||||
("painted clips", painted_clips()),
|
||||
("blend", blend_doc()),
|
||||
]
|
||||
}
|
||||
|
||||
/// A blend of two squares 20 pt apart with a step every 10 pt, written as its steps.
|
||||
fn blend_doc() -> Document {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let keys = vec![
|
||||
Arc::new(square(&mut doc, Rect::new(10.0, 10.0, 12.0, 12.0), Color::gray(0.5))),
|
||||
Arc::new(square(&mut doc, Rect::new(30.0, 10.0, 32.0, 12.0), Color::gray(0.5))),
|
||||
];
|
||||
let spec = vectorcraft_doc::BlendSpec { spacing: vectorcraft_doc::BlendSpacing::Distance(10.0), ..Default::default() };
|
||||
let blend = Node::new(doc.alloc_id(), NodeKind::Blend { children: keys, spec: Box::new(spec) });
|
||||
doc.insert(Some(layer), usize::MAX, blend).unwrap();
|
||||
doc
|
||||
}
|
||||
|
||||
/// Clipping groups whose clipping paths are filled: a square with a color, and a ring (a path of
|
||||
/// two subpaths) with a color, with a gradient, and with a color and a gradient stroke.
|
||||
fn painted_clips() -> Document {
|
||||
let mut doc = Document::new(260.0, 100.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let ring = |x: f64| {
|
||||
let mut p = shapes::rectangle(Rect::new(x, 10.0, x + 20.0, 30.0));
|
||||
p.subpaths.extend(shapes::rectangle(Rect::new(x + 5.0, 15.0, x + 15.0, 25.0)).subpaths);
|
||||
p
|
||||
};
|
||||
let g = Gradient::new(
|
||||
GradientKind::Linear,
|
||||
vec![GradientStop::new(0.0, Color::rgb(1.0, 1.0, 1.0)), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 0.0))],
|
||||
);
|
||||
let shade = || Paint::Gradient(Box::new(GradientPaint::new(g.clone())));
|
||||
let clips = [
|
||||
(10.0, shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None),
|
||||
(70.0, ring(70.0), solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None),
|
||||
(130.0, ring(130.0), shade(), Paint::None),
|
||||
(190.0, ring(190.0), solid(Color::rgb(0.0, 1.0, 0.0)), shade()),
|
||||
];
|
||||
for (x, path, fill, stroke) in clips {
|
||||
let mut clip = clipping(&mut doc, path);
|
||||
let width = if matches!(stroke, Paint::None) { 0.0 } else { 2.0 };
|
||||
clip.appearance = Appearance::basic(fill, stroke, width).into();
|
||||
let inside = square(&mut doc, Rect::new(x - 5.0, 5.0, x + 10.0, 20.0), Color::gray(0.5));
|
||||
let g = group(&mut doc, vec![clip, inside], true);
|
||||
doc.insert(Some(layer), usize::MAX, g).unwrap();
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
/// The writer counts the objects it writes as the reader counts the objects it reads: with room
|
||||
/// for as many objects as the reader reads, nothing is left out; with one less, art is left out,
|
||||
/// with a warning. With room for fewer, the reader never reads more objects than there is room for.
|
||||
#[test]
|
||||
fn the_writer_counts_objects_as_the_reader_reads_them() {
|
||||
let objects = |out: &AiEditingOutput| crate::import::objects_read(&out.program).unwrap();
|
||||
let limited = |out: &AiEditingOutput| out.warnings.iter().any(|w| w == writer::OBJECT_LIMIT);
|
||||
for (name, doc) in counted() {
|
||||
let all = export_ai_editing(&doc, &opts()).unwrap();
|
||||
let read = objects(&all);
|
||||
assert!(read > 0 && !limited(&all), "{name}: {read} objects, {:?}", all.warnings);
|
||||
let room = export_within(&doc, &opts(), read).unwrap();
|
||||
assert!(!limited(&room), "{name}: nothing is left out with room for {read} objects");
|
||||
assert_eq!(room.program, all.program, "{name}");
|
||||
for max in 0..read {
|
||||
let out = export_within(&doc, &opts(), max).unwrap();
|
||||
assert!(limited(&out), "{name}: art is left out with room for {max} of {read} objects");
|
||||
assert!(objects(&out) <= max, "{name}: {} objects read with room for {max}", objects(&out));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An opacity mask whose art doesn't all fit within the object limit is left out with its art,
|
||||
/// and the object it masks is written without it; the art left out leaves room for the objects
|
||||
/// after it.
|
||||
#[test]
|
||||
fn a_mask_whose_art_passes_the_object_limit_is_left_out() {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let mut masked = square(&mut doc, Rect::new(10.0, 10.0, 50.0, 50.0), Color::gray(0.5));
|
||||
let a = square(&mut doc, Rect::new(0.0, 0.0, 30.0, 30.0), Color::rgb(1.0, 1.0, 1.0));
|
||||
let b = square(&mut doc, Rect::new(30.0, 30.0, 60.0, 60.0), Color::rgb(1.0, 1.0, 1.0));
|
||||
let art = group(&mut doc, vec![a, b], false);
|
||||
masked.extras_mut().mask = Some(Box::new(OpacityMask::new(art, false)));
|
||||
doc.insert(Some(layer), usize::MAX, masked).unwrap();
|
||||
let after = square(&mut doc, Rect::new(100.0, 10.0, 140.0, 50.0), Color::gray(0.5));
|
||||
doc.insert(Some(layer), usize::MAX, after).unwrap();
|
||||
let text = |max: usize| {
|
||||
let out = export_within(&doc, &opts(), max).unwrap();
|
||||
let limited = out.warnings.iter().any(|w| w == writer::OBJECT_LIMIT);
|
||||
(String::from_utf8_lossy(&out.program).replace("\r\n", "\n"), limited)
|
||||
};
|
||||
let read = crate::import::objects_read(&export_ai_editing(&doc, &opts()).unwrap().program).unwrap();
|
||||
let (all, limited) = text(read);
|
||||
assert!(!limited && all.contains("(AI9_Mask)"), "{all}");
|
||||
let has_mask = |max: usize| text(max).0.contains("(AI9_Mask)");
|
||||
let Some(fit) = (0..=read).find(|&max| has_mask(max)) else { panic!("the mask fits with room for {read} objects") };
|
||||
assert!(fit > 2, "the mask needs room beyond the two squares: {fit}");
|
||||
for max in fit..=read {
|
||||
assert!(has_mask(max), "room for {max} of {read}, the mask fits in {fit}");
|
||||
}
|
||||
let (short, limited) = text(fit - 1);
|
||||
assert!(limited);
|
||||
assert!(!short.contains("(AI9_Mask)") && !short.contains("%_/ArtDictionary :\n%_/Mask"), "{short}");
|
||||
assert_eq!(short.matches("\nf\n").count(), 2, "both squares are written: {short}");
|
||||
}
|
||||
|
||||
/// A symbol's definition is written only when there is room for its instance too.
|
||||
#[test]
|
||||
fn a_symbol_definition_is_written_only_with_room_for_its_instance() {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let a = square(&mut doc, Rect::new(0.0, 0.0, 10.0, 10.0), Color::gray(0.5));
|
||||
let b = square(&mut doc, Rect::new(10.0, 10.0, 20.0, 20.0), Color::gray(0.5));
|
||||
let art = group(&mut doc, vec![a, b], false);
|
||||
doc.symbols.push(Symbol { dynamic: false, name: "Pair".into(), art: Arc::new(art) });
|
||||
let placed = instance(&mut doc, "Pair", Affine::translate((50.0, 10.0)));
|
||||
doc.insert(Some(layer), usize::MAX, placed).unwrap();
|
||||
add_rect(&mut doc, Rect::new(100.0, 10.0, 110.0, 20.0));
|
||||
let program = export_ai_editing(&doc, &opts()).unwrap().program;
|
||||
let all = String::from_utf8_lossy(&program).replace("\r\n", "\n");
|
||||
assert!(all.contains("%AI14_BeginSymbol") && all.contains("/SymbolInstance :"), "{all}");
|
||||
let read = crate::import::objects_read(&program).unwrap();
|
||||
for max in 0..read {
|
||||
let out = export_within(&doc, &opts(), max).unwrap();
|
||||
let text = String::from_utf8_lossy(&out.program).replace("\r\n", "\n");
|
||||
let defined = text.contains("%AI14_BeginSymbol");
|
||||
assert_eq!(defined, text.contains("/SymbolInstance :"), "room for {max} of {read}: {text}");
|
||||
assert!(out.warnings.iter().any(|w| w == writer::OBJECT_LIMIT), "art is left out with room for {max} of {read} objects");
|
||||
let objects = crate::import::objects_read(&out.program).unwrap();
|
||||
assert!(objects <= max, "{objects} objects read with room for {max}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The writer's limit is the reader's, which its warning names.
|
||||
#[test]
|
||||
fn the_object_limit_is_the_readers() {
|
||||
assert_eq!(crate::import::MAX_EDITING_OBJECTS, 4_000_000);
|
||||
assert!(writer::OBJECT_LIMIT.contains("4,000,000"));
|
||||
}
|
||||
|
||||
/// Paths past the object limit are left out of a layer of many paths, with a warning: the paths
|
||||
/// that fit are written.
|
||||
#[test]
|
||||
fn paths_past_the_object_limit_are_left_out() {
|
||||
let mut doc = Document::new(200.0, 100.0);
|
||||
for i in 0..10 {
|
||||
add_rect(&mut doc, Rect::new(f64::from(i) * 10.0, 0.0, f64::from(i) * 10.0 + 5.0, 5.0));
|
||||
}
|
||||
let out = export_within(&doc, &opts(), 6).unwrap();
|
||||
assert!(out.warnings.iter().any(|w| w == writer::OBJECT_LIMIT), "{:?}", out.warnings);
|
||||
assert_eq!(crate::import::objects_read(&out.program).unwrap(), 6);
|
||||
let text = String::from_utf8_lossy(&out.program).replace("\r\n", "\n");
|
||||
assert_eq!(text.matches("\nf\n").count(), 6, "the first six paths are written");
|
||||
}
|
||||
|
||||
/// On a canvas of the usual size, nested symbols whose instances multiply, written as their art in
|
||||
/// art cut to a clipping object, stop at [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects of symbol art
|
||||
/// in all, with a warning.
|
||||
|
||||
@@ -62,6 +62,7 @@ const GROUPED_LAYER: &str =
|
||||
pub(super) const EFFECTS: &str = "live effects other than geometry effects are left out of the editing data for other apps";
|
||||
pub(super) const SWATCHES: &str = "freeform gradient swatches, gradient swatches of more than 256 stops, and pattern swatches whose patterns the data can't define, are left out of the editing data for other apps";
|
||||
pub(super) const SYMBOL_LIMIT: &str = "symbol instances past 16,000,000 objects of symbol art in all are left out of the editing data for other apps";
|
||||
pub(super) const OBJECT_LIMIT: &str = "art past 4,000,000 objects in all is left out of the editing data for other apps";
|
||||
const TEMPLATES: &str = "template layers are written as dimmed layers that don't print in the editing data for other apps";
|
||||
|
||||
/// The transparency `mode opacity isolate knockout shape Xy` sets: the blending mode ([`blend_code`]),
|
||||
@@ -117,6 +118,16 @@ fn blend_code(b: BlendMode) -> u8 {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the writer had charged when a part of the art started ([`Writer::charges`]).
|
||||
#[derive(Clone, Copy)]
|
||||
struct Charges {
|
||||
objects: usize,
|
||||
symbol_nodes: u64,
|
||||
kept_objects: usize,
|
||||
kept_symbol_nodes: u64,
|
||||
left_out: usize,
|
||||
}
|
||||
|
||||
pub(super) struct Writer<'a> {
|
||||
doc: &'a Document,
|
||||
/// The document → art space.
|
||||
@@ -144,6 +155,18 @@ pub(super) struct Writer<'a> {
|
||||
brushes: Option<Vec<vectorcraft_brush::Brush>>,
|
||||
/// The objects of the symbol art written so far, within [`vectorcraft_doc::MAX_SYMBOL_NODES`].
|
||||
symbol_nodes: u64,
|
||||
/// The objects written so far, counted as the reader counts the objects it reads.
|
||||
objects: usize,
|
||||
/// How many times art was left out for [`Self::max_objects`].
|
||||
left_out: usize,
|
||||
/// The objects of the pattern and symbol definitions written so far, which stay written when
|
||||
/// the art that first used them is not ([`Self::give_back`]).
|
||||
definition_objects: usize,
|
||||
/// The objects of symbol art in those definitions ([`Self::symbol_nodes`]).
|
||||
definition_symbol_nodes: u64,
|
||||
/// The most objects written ([`Self::take_objects`]): the most the reader reads,
|
||||
/// [`crate::import::MAX_EDITING_OBJECTS`].
|
||||
pub(super) max_objects: usize,
|
||||
/// The document → the space gradients' matrices (`Xm`, `Bm`, `Bc`) and focal points are
|
||||
/// written in: art space, and in a pattern's definition [`Self::tile_grad_space`].
|
||||
grad_space: Affine,
|
||||
@@ -286,6 +309,85 @@ impl<'a> Writer<'a> {
|
||||
styled: HashMap::new(),
|
||||
styles: styles::Styles::default(),
|
||||
in_look: false,
|
||||
objects: 0,
|
||||
left_out: 0,
|
||||
definition_objects: 0,
|
||||
definition_symbol_nodes: 0,
|
||||
max_objects: crate::import::MAX_EDITING_OBJECTS,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `count` more objects may be written: the objects written so far, counted as the
|
||||
/// reader counts them ([`object_count`]), stay within [`Self::max_objects`]. Past it the art is
|
||||
/// left out, with a warning.
|
||||
fn take_objects(&mut self, count: usize) -> bool {
|
||||
let total = self.objects.saturating_add(count);
|
||||
if total > self.max_objects {
|
||||
self.warn(OBJECT_LIMIT);
|
||||
self.left_out += 1;
|
||||
return false;
|
||||
}
|
||||
self.objects = total;
|
||||
true
|
||||
}
|
||||
|
||||
/// Start a group with `op` (`u`, or `q` for a clipping group), counted as one object
|
||||
/// ([`Self::take_objects`]). False, writing nothing, when there is no room for it.
|
||||
fn open_group(&mut self, op: &str) -> bool {
|
||||
if !self.take_objects(1) {
|
||||
return false;
|
||||
}
|
||||
self.line(op);
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether `n`, an instance of `symbol` placed by `xf`, is written as a live instance
|
||||
/// ([`Self::live_instance`]). The instance is one object, without the art of its definition,
|
||||
/// and room for it is kept before the definition is written.
|
||||
fn live_instance_in_room(&mut self, n: &Node, symbol: &str, xf: Affine) -> bool {
|
||||
if !self.take_objects(1) {
|
||||
return false;
|
||||
}
|
||||
if self.live_instance(n, symbol, xf) {
|
||||
return true;
|
||||
}
|
||||
self.objects -= 1;
|
||||
false
|
||||
}
|
||||
|
||||
/// What has been charged so far, for [`Self::keep_since`] and [`Self::give_back`].
|
||||
fn charges(&self) -> Charges {
|
||||
Charges {
|
||||
objects: self.objects,
|
||||
symbol_nodes: self.symbol_nodes,
|
||||
kept_objects: self.definition_objects,
|
||||
kept_symbol_nodes: self.definition_symbol_nodes,
|
||||
left_out: self.left_out,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether art was left out for [`Self::max_objects`] since `c`.
|
||||
fn left_out_since(&self, c: Charges) -> bool {
|
||||
self.left_out != c.left_out
|
||||
}
|
||||
|
||||
/// A definition that started at `c` is written: what was charged since stays charged when the
|
||||
/// art that first used it is given back ([`Self::give_back`]).
|
||||
fn keep_since(&mut self, c: Charges) {
|
||||
self.definition_objects = c.kept_objects + (self.objects - c.objects);
|
||||
self.definition_symbol_nodes = c.kept_symbol_nodes + (self.symbol_nodes - c.symbol_nodes);
|
||||
}
|
||||
|
||||
/// Art written since `c` as `lines` is not used: the objects and symbol art charged since are
|
||||
/// given back, except those of the definitions written since, and the images `lines` marks
|
||||
/// are emptied (the others keep their indices).
|
||||
fn give_back(&mut self, c: Charges, lines: &str) {
|
||||
self.objects = c.objects + (self.definition_objects - c.kept_objects);
|
||||
self.symbol_nodes = c.symbol_nodes + (self.definition_symbol_nodes - c.kept_symbol_nodes);
|
||||
for index in lines.split(PAYLOAD).skip(1).step_by(2) {
|
||||
if let Some(p) = index.parse::<usize>().ok().and_then(|i| self.payloads.get_mut(i)) {
|
||||
*p = Vec::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,19 +612,22 @@ impl<'a> Writer<'a> {
|
||||
NodeKind::Layer { .. } => {}
|
||||
NodeKind::Group { children, clip: true } => self.clip_group(children, xy),
|
||||
NodeKind::Group { children, clip: false } => {
|
||||
self.line("u");
|
||||
for c in children {
|
||||
self.node(c);
|
||||
if self.open_group("u") {
|
||||
for c in children {
|
||||
self.node(c);
|
||||
}
|
||||
self.end_group("U", xy);
|
||||
}
|
||||
self.end_group("U", xy);
|
||||
}
|
||||
NodeKind::Path { path, rule, guide, .. } => {
|
||||
if *guide {
|
||||
// Its own transparency, not the one the object before it set.
|
||||
if path.subpaths.iter().any(|sp| !sp.anchors.is_empty()) {
|
||||
// A path for each subpath with points, with the guide's own transparency, not
|
||||
// the one the object before it set.
|
||||
let subpaths = path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()).count();
|
||||
if subpaths > 0 && self.take_objects(subpaths) {
|
||||
self.set_object_xy(xy.object());
|
||||
ai_ops::guide(&mut self.body, self.to_art, path, true);
|
||||
}
|
||||
ai_ops::guide(&mut self.body, self.to_art, path, true);
|
||||
} else {
|
||||
self.shape(n, path, *rule, xy);
|
||||
}
|
||||
@@ -530,8 +635,11 @@ impl<'a> Writer<'a> {
|
||||
NodeKind::Compound { rule, .. } => self.shape(n, &compound_path(n), *rule, xy),
|
||||
NodeKind::Text(_) => self.text(n, xy),
|
||||
NodeKind::Image(im) => self.image(im, xy),
|
||||
NodeKind::SymbolInstance { symbol, xf } if self.live_instance(n, symbol, *xf) => self.symbol_instance(symbol, *xf, xy),
|
||||
NodeKind::SymbolInstance { symbol, xf } if self.live_instance_in_room(n, symbol, *xf) => self.symbol_instance(symbol, *xf, xy),
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
if !self.open_group("u") {
|
||||
return;
|
||||
}
|
||||
// The canvas places an instance inside a symbol's art where the outer instance puts
|
||||
// it (9-slice guides, stains and patterns included), and this art doesn't move with it.
|
||||
self.wrote_fixed |= self.in_symbol;
|
||||
@@ -541,7 +649,6 @@ impl<'a> Writer<'a> {
|
||||
} else {
|
||||
self.warn(symbols::MISSING_SYMBOL);
|
||||
}
|
||||
self.line("u");
|
||||
if let Some(sym) = doc.symbols.iter().find(|s| &s.name == symbol)
|
||||
&& self.nest < MAX_NEST
|
||||
{
|
||||
@@ -583,8 +690,10 @@ impl<'a> Writer<'a> {
|
||||
| NodeKind::Repeat(_)
|
||||
| NodeKind::PlacedDocument(_)
|
||||
| NodeKind::Intertwine { .. } => {
|
||||
if !self.open_group("u") {
|
||||
return;
|
||||
}
|
||||
self.warn(LIVE);
|
||||
self.line("u");
|
||||
if self.nest < MAX_NEST {
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
self.nest += 1;
|
||||
@@ -629,11 +738,14 @@ impl<'a> Writer<'a> {
|
||||
if name.is_none() && mask.is_none() && bound.is_empty() && entries.is_none() {
|
||||
return;
|
||||
}
|
||||
let start = self.body.len();
|
||||
let has_id = name.is_some() || !bound.is_empty();
|
||||
let has_entries = entries.is_some();
|
||||
self.body.push_str("%_/ArtDictionary :\n");
|
||||
if let Some(e) = entries {
|
||||
self.body.push_str(&e);
|
||||
}
|
||||
if name.is_some() || !bound.is_empty() {
|
||||
if has_id {
|
||||
let id = self.take_id(name.unwrap_or("XMLID"));
|
||||
let _ = writeln!(self.body, "%_/XMLUID : {} ; (AI10_ArtUID) ,", ps::bytes(id.as_bytes()));
|
||||
if name.is_none() {
|
||||
@@ -644,8 +756,13 @@ impl<'a> Writer<'a> {
|
||||
self.bound.push((n.id, id, bound));
|
||||
}
|
||||
}
|
||||
if let Some(m) = mask {
|
||||
self.mask(m);
|
||||
// A mask whose art doesn't fit is left out, and with nothing else in it the dictionary too.
|
||||
if let Some(m) = mask
|
||||
&& !self.mask(m)
|
||||
&& !has_id
|
||||
&& !has_entries
|
||||
{
|
||||
return self.body.truncate(start);
|
||||
}
|
||||
self.body.push_str("%_;\n%_\n");
|
||||
}
|
||||
@@ -673,10 +790,13 @@ impl<'a> Writer<'a> {
|
||||
/// An opacity mask, as an entry of its object's dictionary: `/Mask :`, `X=`, its art on `%_`
|
||||
/// lines (from the state a dictionary's art starts in: unlocked, shown, opaque), `X+`, `(Art) ,`,
|
||||
/// its options as the `/Bool` entries `Clipping`, `Inverted`, `Disabled` and `Linked`, and
|
||||
/// `; (AI9_Mask) ,`. The art is in document coordinates, as the object's is.
|
||||
fn mask(&mut self, m: &OpacityMask) {
|
||||
/// `; (AI9_Mask) ,`. The art is in document coordinates, as the object's is. False, writing
|
||||
/// nothing, when some of the art is left out for the object limit ([`Self::take_objects`]): a
|
||||
/// mask without all its art would change what it shows.
|
||||
fn mask(&mut self, m: &OpacityMask) -> bool {
|
||||
let outer = std::mem::take(&mut self.body);
|
||||
let (state, clip) = (self.state, self.clip.take());
|
||||
let (state, gs, clip) = (self.state, self.gs, self.clip.take());
|
||||
let (charges, bound) = (self.charges(), self.bound.len());
|
||||
(self.state, self.gs) = (Some((false, false)), Some(Xy::OPAQUE));
|
||||
// The canvas draws the art's root even when it is hidden, and the reader puts a hidden object
|
||||
// in a group that doesn't draw it, so a hidden root is written shown.
|
||||
@@ -709,6 +829,13 @@ impl<'a> Writer<'a> {
|
||||
self.hidden -= 1;
|
||||
self.nest -= 1;
|
||||
let lines = std::mem::replace(&mut self.body, outer);
|
||||
if self.left_out_since(charges) {
|
||||
// The art isn't written, nor are the variable bindings of its objects.
|
||||
self.give_back(charges, &lines);
|
||||
self.bound.truncate(bound);
|
||||
(self.state, self.gs, self.clip, self.block_end) = (state, gs, clip, usize::MAX);
|
||||
return false;
|
||||
}
|
||||
self.body.push_str("%_/Mask :\n%_X=\n");
|
||||
hide(&lines, &mut self.body);
|
||||
self.body.push_str("%_X+\n%_(Art) ,\n");
|
||||
@@ -718,6 +845,7 @@ impl<'a> Writer<'a> {
|
||||
self.body.push_str("%_; (AI9_Mask) ,\n");
|
||||
// The dictionary ends with the state before it, except the transparency its art set last.
|
||||
(self.state, self.gs, self.clip, self.block_end) = (state, None, clip, usize::MAX);
|
||||
true
|
||||
}
|
||||
|
||||
/// Warn that art is cut to clipping object `first` when it isn't a path or compound path (a path
|
||||
@@ -739,12 +867,16 @@ impl<'a> Writer<'a> {
|
||||
if self.clip.is_some() && !children.is_empty() {
|
||||
self.warn(CLIPPING);
|
||||
}
|
||||
self.line("u");
|
||||
self.clip_contents(children);
|
||||
return self.end_group("U", xy);
|
||||
if self.open_group("u") {
|
||||
self.clip_contents(children);
|
||||
self.end_group("U", xy);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let at_q = (self.state, self.gs);
|
||||
self.line("q");
|
||||
if !self.open_group("q") {
|
||||
return;
|
||||
}
|
||||
self.clipped(children);
|
||||
(self.state, self.gs) = at_q;
|
||||
self.end_group("Q", xy);
|
||||
@@ -753,7 +885,8 @@ impl<'a> Writer<'a> {
|
||||
/// The members of a clipping group or layer, the first one clipping the others: the others,
|
||||
/// then the first one as the clipping path (see [`Self::clip_group`]), its name after it. Fills
|
||||
/// and strokes the clipping path can't paint itself are paths of their own, the fills under the
|
||||
/// others and the strokes over them.
|
||||
/// others and the strokes over them. Without room for the clipping path, nothing is written
|
||||
/// ([`Self::take_objects`]).
|
||||
fn clipped(&mut self, children: &[Arc<Node>]) {
|
||||
let Some((first, rest)) = children.split_first() else { return };
|
||||
let Some((path, rule)) = clip_path(first) else {
|
||||
@@ -762,6 +895,12 @@ impl<'a> Writer<'a> {
|
||||
};
|
||||
let items = self.items(first, path.to_bezpath().bounding_box());
|
||||
let painted = self.plain_paint(&items);
|
||||
// The clipping path goes after the others, so its objects are counted first.
|
||||
let subpaths: Vec<&SubPath> = path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()).collect();
|
||||
let (fill, stroke) = painted.unwrap_or_default();
|
||||
if !self.take_objects(clip_object_count(&subpaths, fill, stroke)) {
|
||||
return;
|
||||
}
|
||||
if items.iter().any(|i| i.opacity() < 1.0 || i.blend() != BlendMode::Normal) {
|
||||
self.warn(ITEM_TRANSPARENCY);
|
||||
}
|
||||
@@ -779,12 +918,10 @@ impl<'a> Writer<'a> {
|
||||
self.node(c);
|
||||
}
|
||||
self.clip_paint_part(first, &path, rule, &strokes);
|
||||
let (fill, stroke) = painted.unwrap_or_default();
|
||||
self.state(first.locked, !first.visible);
|
||||
let start = self.body.len();
|
||||
self.paint_state(fill, stroke, Xy::of(first).object(), rule);
|
||||
let op = paint_op(fill.is_some(), stroke.is_some());
|
||||
let subpaths: Vec<&SubPath> = path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()).collect();
|
||||
let several = subpaths.len() > 1;
|
||||
if several {
|
||||
self.line("*u");
|
||||
@@ -817,7 +954,9 @@ impl<'a> Writer<'a> {
|
||||
if let [one] = items {
|
||||
return self.item(path, rule, one, xy);
|
||||
}
|
||||
self.line("u");
|
||||
if !self.open_group("u") {
|
||||
return;
|
||||
}
|
||||
for i in items {
|
||||
self.item(path, rule, i, Xy::PLAIN);
|
||||
}
|
||||
@@ -870,11 +1009,12 @@ impl<'a> Writer<'a> {
|
||||
(Some((fill, stroke)), _) => self.path_object(path, rule, fill, stroke, xy),
|
||||
(None, [one]) => self.item(path, rule, one, xy),
|
||||
(None, items) => {
|
||||
self.line("u");
|
||||
for i in items {
|
||||
self.item(path, rule, i, Xy::PLAIN);
|
||||
if self.open_group("u") {
|
||||
for i in items {
|
||||
self.item(path, rule, i, Xy::PLAIN);
|
||||
}
|
||||
self.end_group("U", xy);
|
||||
}
|
||||
self.end_group("U", xy);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -898,7 +1038,8 @@ impl<'a> Writer<'a> {
|
||||
}
|
||||
|
||||
/// A brushed stroke `st` of `path` as a group of its brush's art, of transparency `xy`. False
|
||||
/// when it has no brush the document knows (or brushes nest too deep).
|
||||
/// when it has no brush the document knows (or brushes nest too deep). Without room for the
|
||||
/// group ([`Self::take_objects`]), the stroke is left out.
|
||||
fn brush_art(&mut self, path: &PathData, st: &StrokeLayer, xy: Xy) -> bool {
|
||||
if self.nest >= MAX_NEST {
|
||||
return false;
|
||||
@@ -908,8 +1049,10 @@ impl<'a> Writer<'a> {
|
||||
let Some(b) = st.brush.as_deref().and_then(|name| brushes.iter().find(|b| b.name == name)) else { return false };
|
||||
let art = vectorcraft_brush::stroke_pieces(doc, b, &path.to_bezpath(), st);
|
||||
let state = self.state;
|
||||
if !self.open_group("u") {
|
||||
return true;
|
||||
}
|
||||
self.nest += 1;
|
||||
self.line("u");
|
||||
for piece in &art {
|
||||
self.node(piece);
|
||||
}
|
||||
@@ -948,13 +1091,15 @@ impl<'a> Writer<'a> {
|
||||
let flat = FillLayer { paint: Paint::solid(Color::gray(0.5)), ..f.clone() };
|
||||
return self.path_object(path, rule, Some(&flat), None, xy);
|
||||
}
|
||||
let state = self.state;
|
||||
if !self.open_group("u") {
|
||||
return;
|
||||
}
|
||||
self.wrote_pattern = true;
|
||||
let outer = self.clip.clone();
|
||||
let region = self.cut(path, rule);
|
||||
self.clip = Some(region);
|
||||
self.nest += 1;
|
||||
let state = self.state;
|
||||
self.line("u");
|
||||
for t in &tiles {
|
||||
self.node(t);
|
||||
}
|
||||
@@ -979,8 +1124,8 @@ impl<'a> Writer<'a> {
|
||||
}
|
||||
let both = fill.is_some() && stroke.is_some();
|
||||
let own = if both { Xy::OPAQUE } else { xy.object() };
|
||||
if both {
|
||||
self.line("u");
|
||||
if both && !self.open_group("u") {
|
||||
return;
|
||||
}
|
||||
if fill.is_some() || stroke.is_none() {
|
||||
let region = self.cut(path, rule);
|
||||
@@ -1026,7 +1171,7 @@ impl<'a> Writer<'a> {
|
||||
/// points and painting operator (`*u` … `*U` around several).
|
||||
fn plain_object(&mut self, path: &PathData, rule: FillRule, fill: Option<&FillLayer>, stroke: Option<&StrokeLayer>, xy: Xy) {
|
||||
let subpaths: Vec<&SubPath> = path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()).collect();
|
||||
if subpaths.is_empty() {
|
||||
if subpaths.is_empty() || !self.take_objects(object_count(subpaths.len())) {
|
||||
return;
|
||||
}
|
||||
self.paint_state(fill, stroke, xy, rule);
|
||||
@@ -1057,18 +1202,29 @@ impl<'a> Writer<'a> {
|
||||
fn text(&mut self, n: &Node, xy: Xy) {
|
||||
let NodeKind::Text(t) = &n.kind else { return };
|
||||
let doc = self.doc;
|
||||
let placed = if self.hidden > 0 || self.clip.is_some() || self.in_symbol {
|
||||
None
|
||||
} else if let Some(thread) = doc.text_threads.iter().find(|th| th.contains(&n.id)) {
|
||||
let thread = doc.text_threads.iter().find(|th| th.contains(&n.id));
|
||||
// Type that can't be a stub is written as its outlines, which count their own objects.
|
||||
if self.hidden > 0 || self.clip.is_some() || self.in_symbol || (thread.is_none() && !texts::holds(n, t)) {
|
||||
return self.outlined_text(n, xy);
|
||||
}
|
||||
// The stub's objects ([`texts::stub_objects`], [`Self::take_objects`]); without room for
|
||||
// them, the type is left out before its story goes into the text document.
|
||||
let stub = texts::stub_objects(t);
|
||||
if !self.take_objects(stub) {
|
||||
return;
|
||||
}
|
||||
let placed = if let Some(thread) = thread {
|
||||
// A thread is checked for what it loses, in every frame, when its story is written.
|
||||
let placed = self.texts.thread(doc, thread, n.id, self.canvas, &|id| !cut_to_clipping_object(doc, id));
|
||||
placed.map(|p| (p, self.texts.take_lost()))
|
||||
} else if texts::holds(n, t) {
|
||||
self.texts.add(t, self.canvas).map(|story| ((story, 0), texts::loses(t)))
|
||||
} else {
|
||||
None
|
||||
self.texts.add(t, self.canvas).map(|story| ((story, 0), texts::loses(t)))
|
||||
};
|
||||
let Some(((story, frame), lost)) = placed else {
|
||||
// The text document can't hold it: the outlines count their own objects.
|
||||
self.objects -= stub;
|
||||
return self.outlined_text(n, xy);
|
||||
};
|
||||
let Some(((story, frame), lost)) = placed else { return self.outlined_text(n, xy) };
|
||||
self.typed = true;
|
||||
if lost {
|
||||
self.warn(texts::TYPE_ATTRIBUTES);
|
||||
@@ -1088,8 +1244,10 @@ impl<'a> Writer<'a> {
|
||||
/// graphics as their symbols' art where the layout places them, as the PDF page draws them;
|
||||
/// the group has the type's transparency `xy`.
|
||||
fn outlined_text(&mut self, n: &Node, xy: Xy) {
|
||||
if !self.open_group("u") {
|
||||
return;
|
||||
}
|
||||
self.warn(TYPE);
|
||||
self.line("u");
|
||||
let doc = self.doc;
|
||||
let resolved = match &n.kind {
|
||||
NodeKind::Text(t) => Some(doc.inline_resolved(t).into_owned()),
|
||||
@@ -1343,6 +1501,30 @@ fn paint_op(fill: bool, stroke: bool) -> char {
|
||||
}
|
||||
}
|
||||
|
||||
/// The objects the reader makes of a path object of `subpaths` subpaths with points: a path for
|
||||
/// each, and a compound path holding them when there are several.
|
||||
fn object_count(subpaths: usize) -> usize {
|
||||
match subpaths {
|
||||
0 => 0,
|
||||
1 => 1,
|
||||
n => n + 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// The objects the reader counts for clipping path `subpaths` (those with points) painted with
|
||||
/// `fill` and `stroke` ([`Writer::clipped`]). Its last subpath carries a gradient fill's or
|
||||
/// stroke's gradient, so the subpaths of a filled compound clipping path with a gradient paint
|
||||
/// differently, and the reader reads their fills apart from it: a compound path of a copy of each,
|
||||
/// which counts the copy and its points ([`object_count`] for the clipping path itself).
|
||||
fn clip_object_count(subpaths: &[&SubPath], fill: Option<&FillLayer>, stroke: Option<&StrokeLayer>) -> usize {
|
||||
let own = object_count(subpaths.len());
|
||||
let apart = subpaths.len() > 1 && fill.is_some() && (gradient(fill.map(|f| &f.paint)).is_some() || gradient(stroke.map(|s| &s.paint)).is_some());
|
||||
if !apart {
|
||||
return own;
|
||||
}
|
||||
subpaths.iter().fold(own.saturating_add(1), |n, sp| n.saturating_add(1).saturating_add(sp.anchors.len()))
|
||||
}
|
||||
|
||||
/// The subpaths of compound path `n`'s members, as one path.
|
||||
fn compound_path(n: &Node) -> PathData {
|
||||
let mut subpaths = vec![];
|
||||
|
||||
@@ -107,6 +107,11 @@ impl Writer<'_> {
|
||||
if self.clip.as_ref().is_some_and(|c| c.subpaths.iter().all(|sp| sp.anchors.is_empty())) {
|
||||
return;
|
||||
}
|
||||
// The image, and where it is cut, the clipping group and its clipping path.
|
||||
let clipping = self.clip.as_ref().map_or(0, |c| 1 + super::object_count(c.subpaths.iter().filter(|sp| !sp.anchors.is_empty()).count()));
|
||||
if !self.take_objects(1 + clipping) {
|
||||
return;
|
||||
}
|
||||
if deep {
|
||||
self.warn(IMAGE_DEPTH);
|
||||
}
|
||||
|
||||
@@ -381,7 +381,8 @@ impl Writer<'_> {
|
||||
/// Write pattern `name`'s definition, once: `%AI3_BeginPattern: (name)`, `(name) llx lly urx
|
||||
/// ury` (the tile, y up), the tile's box (an unpainted path whose dictionary marks it,
|
||||
/// `AIPattern_Editor_Backing_Tile_Rect`) and the art on `%_` lines, `E`, `%AI3_EndPattern`.
|
||||
/// False when it can't be ([`Self::pattern_writable`]).
|
||||
/// False when it can't be ([`Self::pattern_writable`]), or when its box and art don't all fit
|
||||
/// within the object limit ([`Self::take_objects`]).
|
||||
fn define_pattern(&mut self, name: &str) -> bool {
|
||||
if !self.pattern_writable(name) {
|
||||
return false;
|
||||
@@ -392,7 +393,9 @@ impl Writer<'_> {
|
||||
return true;
|
||||
}
|
||||
let doc = self.doc;
|
||||
let Some(def) = doc.pattern(name) else { return false };
|
||||
// The tile's box is an object of the definition ([`Self::take_objects`]).
|
||||
let (before, bound) = (self.charges(), self.bound.len());
|
||||
let Some(def) = doc.pattern(name).filter(|_| self.take_objects(1)) else { return false };
|
||||
self.defining.push(name.to_string());
|
||||
// The art is written in the pattern's space, y up, from the state a definition starts in.
|
||||
let saved = (
|
||||
@@ -427,6 +430,12 @@ impl Writer<'_> {
|
||||
(art, (self.to_art, self.grad_space, self.canvas), self.state, self.gs, self.clip, self.block_end) = saved;
|
||||
let art = std::mem::replace(&mut self.body, art);
|
||||
self.defining.pop();
|
||||
if self.left_out_since(before) {
|
||||
self.give_back(before, &art);
|
||||
self.bound.truncate(bound);
|
||||
return false;
|
||||
}
|
||||
self.keep_since(before);
|
||||
let tile = Affine::FLIP_Y.transform_rect_bbox(def.tile);
|
||||
let quoted = ps::bytes(name.as_bytes());
|
||||
let mut text = format!("%AI3_BeginPattern: {quoted}\n{quoted} ");
|
||||
|
||||
@@ -688,7 +688,18 @@ impl Writer<'_> {
|
||||
/// after the group's end, `0 0 Xd`, `6 () XW`); the object itself on `%_` lines (a path's or
|
||||
/// compound path's points painted with nothing, or with its gradient fill when its style has
|
||||
/// one; a group with its members), its name and opacity mask; then `1 (name) XW`.
|
||||
/// The look's group and the object itself are counted first ([`Self::take_objects`]): without
|
||||
/// room for them, nothing is written.
|
||||
pub(super) fn styled_object(&mut self, n: &Node, orig: &Node, name: &str) {
|
||||
let own = match &orig.kind {
|
||||
NodeKind::Path { path, .. } => super::object_count(path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()).count()),
|
||||
NodeKind::Compound { .. } => super::object_count(compound_path(orig).subpaths.iter().filter(|sp| !sp.anchors.is_empty()).count()),
|
||||
NodeKind::Group { .. } => 1,
|
||||
_ => 0,
|
||||
};
|
||||
if !self.take_objects(1 + own) {
|
||||
return;
|
||||
}
|
||||
self.state(orig.locked, !orig.visible);
|
||||
let xy = Xy::of(orig);
|
||||
let was = self.in_look;
|
||||
@@ -717,6 +728,10 @@ impl Writer<'_> {
|
||||
let saved = std::mem::take(&mut self.body);
|
||||
self.hidden += 1;
|
||||
let fit = drawn_outline(n, orig).map(|p| p.bounding_box());
|
||||
// A path's objects were kept for it above; it counts them itself.
|
||||
if !matches!(orig.kind, NodeKind::Group { .. }) {
|
||||
self.objects -= own;
|
||||
}
|
||||
match &orig.kind {
|
||||
NodeKind::Path { path, rule, .. } => self.styled_outline(orig, path, *rule, fit, xy),
|
||||
NodeKind::Compound { rule, .. } => self.styled_outline(orig, &compound_path(orig), *rule, fit, xy),
|
||||
|
||||
@@ -183,8 +183,9 @@ impl Writer<'_> {
|
||||
/// written from the top of the nesting whatever the instance that asks for it, so a definition
|
||||
/// is the same wherever its first instance lies. False when it can't be: the document doesn't
|
||||
/// have the symbol, it is being written (its art holds an instance of itself), it is reached
|
||||
/// through more than [`MAX_NEST`] definitions being written, or the definitions written and
|
||||
/// being written leave no place under [`MAX_SYMBOLS`].
|
||||
/// through more than [`MAX_NEST`] definitions being written, the definitions written and
|
||||
/// being written leave no place under [`MAX_SYMBOLS`], or its art doesn't all fit within the
|
||||
/// object limit ([`Self::take_objects`]).
|
||||
pub(super) fn define_symbol(&mut self, name: &str) -> bool {
|
||||
if self.defined_symbols.contains(name) {
|
||||
return true;
|
||||
@@ -206,6 +207,8 @@ impl Writer<'_> {
|
||||
// The art's effects are its own, not those of a drawn look that first places it.
|
||||
let in_look = std::mem::take(&mut self.in_look);
|
||||
self.defining_symbols.push(name.to_string());
|
||||
// The objects of this definition and of those it defines first.
|
||||
let before = self.charges();
|
||||
let mut used = vec![];
|
||||
vectorcraft_doc::swatches::walk_with_masks(&sym.art, &mut |m| {
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &m.kind {
|
||||
@@ -217,10 +220,19 @@ impl Writer<'_> {
|
||||
}
|
||||
if self.symbol_defs.len() + self.defining_symbols.len() > MAX_SYMBOLS {
|
||||
self.defining_symbols.pop();
|
||||
self.keep_since(before);
|
||||
self.warn(SYMBOL_COUNT);
|
||||
self.in_look = in_look;
|
||||
return false;
|
||||
}
|
||||
// The group that holds the art is an object of the definition ([`Self::take_objects`]);
|
||||
// without room for it, or for all the art, there is no definition.
|
||||
let (art_start, bound) = (self.charges(), self.bound.len());
|
||||
if !self.take_objects(1) {
|
||||
self.defining_symbols.pop();
|
||||
self.keep_since(before);
|
||||
return false;
|
||||
}
|
||||
// The art is written in the symbol's space, from the state `0 A` and `0 Xw` set, from the
|
||||
// document at its own size.
|
||||
let origin = self.canvas_origin;
|
||||
@@ -264,7 +276,9 @@ impl Writer<'_> {
|
||||
let _ = write!(self.body, "U\n%_/ArtDictionary :\n{entries}%_;\n%_\n9 () XW\n");
|
||||
} else {
|
||||
// A group always writes its dictionary ([`Self::dictionary`]), which takes the entries.
|
||||
// It counts itself, in the room kept for it above.
|
||||
self.root_entries = Some((root.id, entries));
|
||||
self.objects -= 1;
|
||||
self.node(root);
|
||||
}
|
||||
self.nest = nest;
|
||||
@@ -297,6 +311,13 @@ impl Writer<'_> {
|
||||
self.in_look = in_look;
|
||||
let art = std::mem::replace(&mut self.body, art);
|
||||
self.defining_symbols.pop();
|
||||
if self.left_out_since(art_start) {
|
||||
self.give_back(art_start, &art);
|
||||
self.bound.truncate(bound);
|
||||
self.keep_since(before);
|
||||
return false;
|
||||
}
|
||||
self.keep_since(before);
|
||||
let text = format!("%AI14_BeginSymbol\n{}\n0 A\n0 Xw\n0 Ae\n{art}%AI10_EndSymbol\n", ps::bytes(name.as_bytes()));
|
||||
self.defined_symbols.insert(name.to_string());
|
||||
self.symbol_defs.push(text);
|
||||
|
||||
@@ -1016,6 +1016,12 @@ fn clone_v(v: &V) -> V {
|
||||
}
|
||||
}
|
||||
|
||||
/// The objects the reader reads of [`stub`] for `t`: the text object, and a path for each subpath
|
||||
/// of its frame's path.
|
||||
pub(in crate::ai_editing) fn stub_objects(t: &TextObject) -> usize {
|
||||
1 + shape(t).map_or(0, |p| p.subpaths.iter().filter(|sp| !sp.anchors.is_empty()).count())
|
||||
}
|
||||
|
||||
/// The text object stub of story `story`, frame `frame`: `/AI11Text :`, `/FreeUndo`, `/FrameIndex`,
|
||||
/// `/StoryIndex`, for area type and type on a path the frame's path (`/Art :`, `X=`, the path in
|
||||
/// art space through `to_art`, `X+`, `; /ConfiningPath ,`), `/TextAntialiasing`, `;`.
|
||||
|
||||
@@ -73,8 +73,11 @@ use graphics::{GState, Out};
|
||||
use interp::{Fault, Interp};
|
||||
use obj::PsError;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use ai::objects_read;
|
||||
pub(crate) use ai::{
|
||||
MAX_GRADIENT_STOPS, MAX_LISTED, MAX_LISTED_NAME, MAX_SYMBOLS, read_art_style, read_style_list, style_moves_with, symbol_art_member, warp_number,
|
||||
MAX_EDITING_OBJECTS, MAX_GRADIENT_STOPS, MAX_LISTED, MAX_LISTED_NAME, MAX_SYMBOLS, read_art_style, read_style_list, style_moves_with,
|
||||
symbol_art_member, warp_number,
|
||||
};
|
||||
pub(crate) use ate::{MAX_BYTES as MAX_TEXT_DOCUMENT, MAX_FRAMES, MAX_SEGMENTS, MAX_STORY, MAX_VALUES as MAX_TEXT_VALUES};
|
||||
pub use native::{AiLayers, ai_alone, ai_alone_on_page, ai_layers_alone, ai_opened_artboard, is_loss, layered_ai, layered_ai_from};
|
||||
|
||||
@@ -41,13 +41,16 @@ pub(crate) use envelope::{WARPED_TYPE_DRAWN, WarpedType};
|
||||
pub(crate) use frames::text_frames;
|
||||
pub(crate) use paint::MAX_STOPS as MAX_GRADIENT_STOPS;
|
||||
#[cfg(test)]
|
||||
pub(crate) use read::objects_read;
|
||||
#[cfg(test)]
|
||||
pub(crate) use read::with_image_limits;
|
||||
pub use read::{
|
||||
BindingRef, Brushes, PaletteSwatches, PlacedPage, Structure, Symbols, active_artboard, artboards, bindings_note, brushes, graphic_styles,
|
||||
palette, read, read_beside, symbols, variables,
|
||||
};
|
||||
pub(crate) use read::{
|
||||
MAX_SYMBOLS, join_and, read_art_style, read_over_page, read_style_list, slot_frame, slot_of, style_moves_with, symbol_art_member,
|
||||
MAX_NODES as MAX_EDITING_OBJECTS, MAX_SYMBOLS, join_and, read_art_style, read_over_page, read_style_list, slot_frame, slot_of, style_moves_with,
|
||||
symbol_art_member,
|
||||
};
|
||||
|
||||
pub(crate) use read::is_look_slot;
|
||||
|
||||
@@ -149,6 +149,7 @@ pub(crate) use document::{slot_frame, slot_of};
|
||||
pub(crate) use ops::{read_art_style, read_style_list, style_moves_with};
|
||||
#[cfg(test)]
|
||||
pub(crate) use raster::with_image_limits;
|
||||
pub(crate) use types::MAX_NODES;
|
||||
pub use types::PlacedPage;
|
||||
use types::{BLEND_BUDGET, BrushArtColors, FLIP, Frame, GState, Graph, Kind, PatternHead, PendingEnvelope, Place, PlacedTail, Raster, SymbolHead};
|
||||
#[cfg(test)]
|
||||
@@ -259,6 +260,14 @@ fn read_with(data: &[u8], large_canvas: bool, page_art: bool) -> Result<Structur
|
||||
r.finish()
|
||||
}
|
||||
|
||||
/// How many objects [`read`] counts against [`MAX_NODES`] in the editing data `data`.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn objects_read(data: &[u8]) -> Result<usize, String> {
|
||||
let mut r = Reader::with_names(data);
|
||||
r.run()?;
|
||||
Ok(r.nodes)
|
||||
}
|
||||
|
||||
/// [`read`] for editing data kept apart from the program `ps` it came with (an EPS's private
|
||||
/// data): the program's header gives the author and the custom colors the editing data's header
|
||||
/// doesn't. `page_art`: as [`read_over_page`] reads a file.
|
||||
|
||||
@@ -11,8 +11,8 @@ pub(super) const MAX_SUBREADERS: usize = 32;
|
||||
/// Most operands on the stack.
|
||||
pub(super) const MAX_STACK: usize = 1 << 20;
|
||||
|
||||
/// Most objects read.
|
||||
pub(super) const MAX_NODES: usize = 4_000_000;
|
||||
/// Most objects read. The `.ai` editing data writer writes no more ([`crate::import::MAX_EDITING_OBJECTS`]).
|
||||
pub(crate) const MAX_NODES: usize = 4_000_000;
|
||||
|
||||
/// The most objects all the blends of one import may lay out to be checked (each twice) and made
|
||||
/// live; past it, blends keep their drawn steps.
|
||||
|
||||
Reference in New Issue
Block a user