From bf716bfeccc87b141cf69741cf93507fd88aeec6 Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 15:49:23 +0000 Subject: [PATCH] .ai import: envelopes of type keep their drawn art in layers used though they differ from the page An envelope of type opens live only where the layers with it draw as the page. Two ways of using layers that don't left it live all the same: the editing data trusted (they come from it with a note), and the objects that draw differently coming in as the page's art. In both the envelope now keeps its drawn art and its type, hidden, with the note, as where its drawn art is put back. A .ai whose layers would open alone, without its PDF part, then opens alone again, as it did before envelopes of type could open live, rather than compared with the page. The type put back no longer carries the mark of type made from the text document in its name. Part of #3179. Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/native/build.rs | 41 ++++++++++++-- crates/eps/src/import/tests_native.rs | 79 +++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/crates/eps/src/import/native/build.rs b/crates/eps/src/import/native/build.rs index 4266b945c..e2480d6c5 100644 --- a/crates/eps/src/import/native/build.rs +++ b/crates/eps/src/import/native/build.rs @@ -493,9 +493,14 @@ pub(super) fn build_from( other => match drawn_or_trusted(&frame, &mut view, &mut doc, &envelopes_drawn, ¬es, unseen, trust, why, reflow) { Ok(more) => notes.extend(more), // The objects that draw differently from the page come in as its art, from - // the placement that draws closer to it. + // the placement that draws closer to it. The layers don't draw as the page: + // envelopes of type keep their drawn art and their type, hidden. Err(why) => { - let other = other.map(|(_, other, unseen)| (other, unseen)); + let typed = type_drawn(&mut doc, &envelopes_drawn); + let other = other.map(|(_, mut other, unseen)| { + type_drawn(&mut other, &envelopes_drawn); + (other, unseen) + }); let placements = closer_first(&frame, &mut view, (doc, unseen), other, reflow); let Some((kept, unseen, kept_notes)) = placements.into_iter().find_map(|(base, unseen)| { keep_differing(&base, &frame, &mut view, &threaded_frames, &shown, reflow).map(|(kept, n)| (kept, unseen, n)) @@ -503,7 +508,8 @@ pub(super) fn build_from( return Err(why); }; doc = kept; - notes.extend(unseen_note(unseen).into_iter().chain(kept_notes)); + let typed = typed.then(|| ai::ENVELOPE_TYPE_DRAWN.to_string()).filter(|n| !notes.contains(n)); + notes.extend(typed.into_iter().chain(unseen_note(unseen)).chain(kept_notes)); } }, }, @@ -1287,8 +1293,9 @@ fn slot_content( /// The layers `doc` don't draw as the page (`why`), `unseen` text objects of them taken out: the /// notes to add when `doc` with each live envelope `drawn` lists replaced by the drawn art it took /// the place of (see [`ai::Structure::envelopes_drawn`]) draws as the page, `doc` then being that; -/// else, with `trust`, the notes for `doc` as it is. An error, `why`, otherwise. The envelope note -/// is added unless `notes` has it. +/// else, with `trust`, the notes for `doc` as it is but for its envelopes of type, which keep their +/// drawn art ([`type_drawn`]). An error, `why`, otherwise. The envelope note is added unless +/// `notes` has it. #[allow(clippy::too_many_arguments)] fn drawn_or_trusted( frame: &Frame<'_>, @@ -1320,11 +1327,33 @@ fn drawn_or_trusted( } } if trust { - return Ok(unseen_note(unseen).into_iter().chain([format!("{why}: they come from its editing data all the same")]).collect()); + let typed = type_drawn(doc, drawn).then(|| ai::ENVELOPE_TYPE_DRAWN.to_string()).filter(|n| !notes.contains(n)); + let trusted = format!("{why}: they come from its editing data all the same"); + return Ok(typed.into_iter().chain(unseen_note(unseen)).chain([trusted]).collect()); } Err(why) } +/// Put each live envelope of type in `drawn` (as [`drawn_or_trusted`] takes them) that `doc` still +/// has back as its hidden type and its drawn art: whether one was. An envelope of type opens live +/// only where the layers with it draw as the page, so where they don't, though they are used all +/// the same (the editing data trusted, or the objects that differ come in as the page's art), it +/// keeps its drawn art. +fn type_drawn(doc: &mut Document, drawn: &[(NodeId, Vec, &str)]) -> bool { + let typed: Vec<(NodeId, Vec)> = drawn + .iter() + .filter(|(id, _, say)| *say == ai::ENVELOPE_TYPE_DRAWN && doc.node(*id).is_some()) + .map(|(id, art, _)| (*id, art.clone())) + .collect(); + if typed.is_empty() || doc.insert_beside(typed, true).is_err() { + return false; + } + // The type put back is still marked as made from the text document, which the rest no longer + // is once the page has weighed it ([`prune_unseen`]). + unmark(&mut doc.layers, true, true, &mut vec![]); + true +} + /// `doc` with the placed files `placed` lists at their other placement (see /// [`ai::Structure::placed_elsewhere`]): each image's transform followed by its move. `None` when /// no listed image survives replacement. diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index ffa209a84..6a2723dc9 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -5473,6 +5473,20 @@ fn an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone() { assert_eq!(loose, [("Note".to_string(), true)], "{notes:?}"); } +/// The hidden type outside envelopes in the layers of `d`: each text object's name. +fn loose_type_names(d: &Document) -> Vec> { + let mut out = vec![]; + fn walk(n: &Node, out: &mut Vec>) { + match &n.kind { + NodeKind::Envelope { .. } => {} + NodeKind::Text(_) => out.push(n.name.clone()), + _ => n.children().into_iter().flatten().for_each(|c| walk(c, out)), + } + } + d.layers.iter().for_each(|l| walk(l, &mut out)); + out +} + /// An envelope of type that draws as its drawn art doesn't open live where the page draws other art /// there (the warp bent the other way): the page check weighs the live envelope as the art it draws, /// not as type laid out again, so it sees the two differ. The file comes in as its page, as it does @@ -5491,6 +5505,71 @@ fn an_envelope_of_type_that_draws_apart_from_the_page_does_not_open_live() { assert!(r.warnings.iter().any(|n| n.contains("layers weren't read") && n.contains("differs from the page's")), "{:?}", r.warnings); } +/// Layers used though they draw differently from the page (the editing data trusted) leave an +/// envelope of type its drawn art and its type, hidden, with the note, even where it draws as that +/// drawn art: one opens live only where the layers with it draw as the page. A `.ai` whose layers +/// would open alone then opens alone, as it would without the envelope (#3179). +#[test] +fn trusted_layers_that_draw_apart_from_the_page_leave_an_envelope_of_type_its_drawn_art() { + let Some(family) = installed_family() else { return }; + let doc = type_envelope_text(&family, None); + let (drawn, _) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), 30.0)); + let art = native_with_text(&layer("Art", true, &format!("{}\n{}", square(10, 10), type_envelope(&arc_keys(0.3), &drawn))), &doc); + // The page has the square only. + let r = crate::import_trusting(&eps(&page_square(10, 10), &art), true, true).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + let (envelopes, loose) = envelopes_and_type(&r.document); + assert!(envelopes.is_empty(), "{envelopes:?}"); + assert_eq!(loose, [("FLUX".to_string(), false)], "{:?}", r.warnings); + // The type put back is named as the file names it. + assert_eq!(loose_type_names(&r.document), [None]); + assert!(r.warnings.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{:?}", r.warnings); + assert!(r.warnings.iter().any(|n| n.ends_with("they come from its editing data all the same")), "{:?}", r.warnings); + // A `.ai` whose layers would open alone, its PDF part drawn apart from them. + let stories = [("FLUX\r", 0, (8170.0, 8180.0), (0.0, 0.0), 0.0), ("Note\r", 0, (8170.0, 8220.0), (0.0, 0.0), 0.0)]; + let mut doc = text_document(&stories).replace("(Helvetica)", &format!("({family})")); + while ascii85(doc.as_bytes()).len() % 60 == 1 { + doc.push('\n'); + } + let art = format!("u\n{}U\n{}", text_object(1), type_envelope(&arc_keys(0.3), &drawn)); + let file = private(&native_with_text(&layer("Art", true, &art), &doc)); + let names = crate::NameEncoding::default(); + let Err(layers) = crate::ai_layers_alone(&file, &[(100.0, 100.0)], 1.0, names) else { panic!("opened without the page") }; + let (d, notes, read) = crate::layered_ai_from(layers, pdf_part(80.0, 80.0), vec![], false, true, 1.0, names); + assert!(read, "{notes:?}"); + let (envelopes, loose) = envelopes_and_type(&d); + assert!(envelopes.is_empty(), "{envelopes:?}"); + assert_eq!(loose, [("Note".to_string(), true), ("FLUX".to_string(), false)], "{notes:?}"); + assert!(notes.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{notes:?}"); + assert!(!notes.iter().any(|n| n.ends_with("they come from its editing data all the same")), "{notes:?}"); +} + +/// Layers whose objects that draw differently from the page come in as its art leave an envelope +/// of type its drawn art and its type, hidden, with the note, even where the envelope draws as the +/// page: the layers with it don't (#3179). +#[test] +fn layers_with_objects_that_come_in_as_the_page_art_leave_an_envelope_of_type_its_drawn_art() { + let Some(family) = installed_family() else { return }; + let doc = type_envelope_text(&family, None); + let (drawn, page) = drawn_envelope(&arc_envelope(&made_envelope_type(&doc), 30.0)); + // A square black in the layers and cyan on the page, away from the envelope. + let body = format!("{}\n{}\n{}", square(10, 10), square_16(76, BLACK), type_envelope(&arc_keys(0.3), &drawn)); + let art = native_with_text(&layer("Art", true, &body), &doc); + let cyan = "1 0 0 0 setcmykcolor 76 10 moveto 92 10 lineto 92 26 lineto 76 26 lineto closepath fill"; + let r = import(&eps(&format!("{}\n{cyan}\n{page}", page_square(10, 10)), &art)).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + assert!( + r.warnings.iter().any(|n| n.starts_with("objects that drew differently from the file's own page came in as its art")), + "{:?}", + r.warnings + ); + let (envelopes, loose) = envelopes_and_type(&r.document); + assert!(envelopes.is_empty(), "{envelopes:?}"); + assert_eq!(loose, [("FLUX".to_string(), false)], "{:?}", r.warnings); + assert_eq!(loose_type_names(&r.document), [None]); + assert!(r.warnings.iter().any(|n| n == crate::import::ai::ENVELOPE_TYPE_DRAWN), "{:?}", r.warnings); +} + /// The fill of the first run of type `n`. fn text_fill(n: &Node) -> Option { let NodeKind::Text(t) = &n.kind else { return None };