mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
.ai import: outlines from the editing data keep their look and threads; mesh corners with non-numbers are unread
Follow-ups to the review of #4697 and #4698: - With textAs: outlines, a .ai saved without PDF compatibility, read from its editing data alone, has its type outlined too, as an empty page or a text-only placeholder already did. - Threaded type read from the editing data holds its story in its first frame: it now flows through its frames before it is outlined, so each frame's outlines show the text it holds, and the outlined frames leave their threads. - Each outline group keeps its type's opacity, blend mode, isolation, opacity mask and effects (the same carry-over the flattener uses). - A gradient-mesh corner (/N) holding anything but numbers is unread, instead of being read with the numbers after it shifted into its colour and opacity.
This commit is contained in:
@@ -204,11 +204,18 @@ fn is_page_note(w: &str) -> bool {
|
||||
}
|
||||
|
||||
/// The document read from the editing data alone with its type as outlines when `opts` asks for
|
||||
/// them (`textAs: outlines`): the page that would have them has none to give.
|
||||
fn outlined_if_asked(mut doc: Document, warnings: Vec<String>, opts: &LoadOptions) -> (Document, Vec<String>) {
|
||||
/// them (`textAs: outlines`): the page that would have them has none to give. Threaded type first
|
||||
/// flows through its frames (the editing data gives a story in its first frame), and the outlined
|
||||
/// frames leave their threads. Each outline group keeps its type's transparency, opacity mask and
|
||||
/// effects.
|
||||
pub(super) fn outlined_if_asked(mut doc: Document, warnings: Vec<String>, opts: &LoadOptions) -> (Document, Vec<String>) {
|
||||
if opts.text_as != vectorcraft_pdf::TextAs::Outlines {
|
||||
return (doc, warnings);
|
||||
}
|
||||
if !doc.text_threads.is_empty() {
|
||||
crate::cmd::threads::reflow(&Document::new(1.0, 1.0), &mut doc);
|
||||
}
|
||||
let threaded = crate::cmd::threads::frames(&doc);
|
||||
let mut texts = Vec::new();
|
||||
doc.walk(|n| {
|
||||
if matches!(n.kind, vectorcraft_doc::NodeKind::Text(_)) {
|
||||
@@ -216,12 +223,13 @@ fn outlined_if_asked(mut doc: Document, warnings: Vec<String>, opts: &LoadOption
|
||||
}
|
||||
});
|
||||
for t in texts {
|
||||
let Some(o) = vectorcraft_render::effects::outline_text(&t) else { continue };
|
||||
let o = Node { visible: t.visible, locked: t.locked, name: t.name.clone(), ..doc.reid(&o) };
|
||||
let Some(o) = crate::cmd::flatten::outlined_text(&t) else { continue };
|
||||
let o = Node { visible: t.visible, locked: t.locked, ..doc.reid(&o) };
|
||||
if let Some(slot) = doc.node_mut(t.id) {
|
||||
*slot = o;
|
||||
}
|
||||
}
|
||||
crate::cmd::threads::leave_threads(&mut doc, &threaded);
|
||||
(doc, warnings)
|
||||
}
|
||||
|
||||
@@ -331,6 +339,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
let whole = LoadOptions { pages: None, ..opts.clone() };
|
||||
from_editing_data_alone(bytes, if every_page(bytes, opts) { &whole } else { opts })
|
||||
.unwrap_or(Err(e))
|
||||
.map(|(doc, warnings)| outlined_if_asked(doc, warnings, opts))
|
||||
.inspect(|_| editing_data = true)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::io::Write as _;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_doc::{Node, NodeId, NodeKind};
|
||||
use vectorcraft_testkit::ai;
|
||||
use vectorcraft_testkit::pdf::{PdfPage, first_extra, pdf_with};
|
||||
|
||||
@@ -589,12 +589,10 @@ fn a_page_whose_content_was_skipped_is_not_taken_for_an_empty_one() {
|
||||
assert!(!warnings.contains("saved without PDF compatibility"), "{r}");
|
||||
}
|
||||
|
||||
/// Type asked for as outlines on a page that draws nothing: the editing data's type, outlined (the
|
||||
/// page has none to give).
|
||||
#[test]
|
||||
fn an_empty_page_with_type_as_outlines_outlines_the_editing_datas_type() {
|
||||
// One left-aligned story, "Hi", 14 pt Helvetica, anchored at the canvas centre (the template
|
||||
// box's centre), as the editing data's text document gives it.
|
||||
/// Editing data with one layer, "Art", holding one left-aligned story, "Hi", 14 pt Helvetica,
|
||||
/// anchored at the canvas centre (the template box's centre), as the editing data's text document
|
||||
/// gives it; compressed.
|
||||
fn typed_data() -> Vec<u8> {
|
||||
let story = "<< /0 << /0 (Hi\r) /5 << /0 [ << /0 << /0 << /0 () /5 << /0 0 >> /6 0 >> >> /1 3 >> ] >> \
|
||||
/6 << /0 [ << /0 << /0 << /0 () /5 0 /6 << /0 0 /1 14.0 /53 << /99 /CAITextPaint /0 << /0 2 /1 [ 1.0 0.0 1.0 0.0 0.0 ] >> >> >> >> >> /1 3 >> ] >> >> \
|
||||
/1 << /0 [ << /0 0 >> ] /2 [ << /99 /PC /6 [ << /99 /F /0 << /0 [ 8191.5 8191.5 ] >> /6 [ << /99 /R /6 [ << /99 /R /6 [ << /99 /L /6 [ \
|
||||
@@ -610,23 +608,121 @@ fn an_empty_page_with_type_as_outlines_outlines_the_editing_datas_type() {
|
||||
ai::layer("Art", text),
|
||||
lines.join("\n")
|
||||
);
|
||||
let file = ai::ai(&compressed(data.as_bytes()), "");
|
||||
let texts = |s: &Session| {
|
||||
let mut n = 0;
|
||||
s.doc().unwrap().doc.walk(|c| n += usize::from(matches!(c.kind, NodeKind::Text(_))));
|
||||
n
|
||||
};
|
||||
compressed(data.as_bytes())
|
||||
}
|
||||
|
||||
/// How many text objects and how many paths the open document holds.
|
||||
fn texts_and_paths(s: &Session) -> (usize, usize) {
|
||||
let (mut texts, mut paths) = (0, 0);
|
||||
s.doc().unwrap().doc.walk(|c| {
|
||||
texts += usize::from(matches!(c.kind, NodeKind::Text(_)));
|
||||
paths += usize::from(matches!(c.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }));
|
||||
});
|
||||
(texts, paths)
|
||||
}
|
||||
|
||||
/// Type asked for as outlines on a page that draws nothing: the editing data's type, outlined (the
|
||||
/// page has none to give).
|
||||
#[test]
|
||||
fn an_empty_page_with_type_as_outlines_outlines_the_editing_datas_type() {
|
||||
let file = ai::ai(&typed_data(), "");
|
||||
let mut s = Session::new();
|
||||
let r = open(&mut s, "typed.ai", &file, json!({})).unwrap();
|
||||
assert_eq!(texts(&s), 1, "by default the editing data's type is type: {r}");
|
||||
assert_eq!(texts_and_paths(&s).0, 1, "by default the editing data's type is type: {r}");
|
||||
let r = open(&mut s, "typed.ai", &file, json!({"textAs": "outlines"})).unwrap();
|
||||
assert_eq!(texts(&s), 0, "no type left: {r}");
|
||||
let mut paths = 0;
|
||||
s.doc().unwrap().doc.walk(|c| paths += usize::from(matches!(c.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })));
|
||||
let (texts, paths) = texts_and_paths(&s);
|
||||
assert_eq!(texts, 0, "no type left: {r}");
|
||||
assert!(paths > 0, "the outlines are there: {r}");
|
||||
assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "the editing data's layers");
|
||||
}
|
||||
|
||||
/// Type asked for as outlines in a `.ai` saved without PDF compatibility (its page only a
|
||||
/// placeholder): the editing data's type, outlined, as for an empty page.
|
||||
#[test]
|
||||
fn a_file_saved_without_its_pdf_part_with_type_as_outlines_outlines_its_type() {
|
||||
let file = ai::ai(&typed_data(), PLACEHOLDER);
|
||||
let mut s = Session::new();
|
||||
let r = open(&mut s, "typed.ai", &file, json!({})).unwrap();
|
||||
assert_eq!(texts_and_paths(&s).0, 1, "by default the editing data's type is type: {r}");
|
||||
let r = open(&mut s, "typed.ai", &file, json!({"textAs": "outlines"})).unwrap();
|
||||
let (texts, paths) = texts_and_paths(&s);
|
||||
assert_eq!(texts, 0, "no type left: {r}");
|
||||
assert!(paths > 0, "the outlines are there: {r}");
|
||||
assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "the editing data's layers");
|
||||
}
|
||||
|
||||
/// Type of the editing data outlined keeps its transparency, opacity mask and effects, and is
|
||||
/// shown and locked as the type was.
|
||||
#[test]
|
||||
fn outlined_type_of_the_editing_data_keeps_its_transparency_and_effects() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 100})).unwrap();
|
||||
let id = NodeId(s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hi"})).unwrap()["id"].as_u64().unwrap());
|
||||
let mut doc = s.doc().unwrap().doc.as_ref().clone();
|
||||
let shadow = vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params: json!({}), visible: true };
|
||||
{
|
||||
let t = doc.node_mut(id).unwrap();
|
||||
t.opacity = 0.5;
|
||||
t.blend = vectorcraft_color::BlendMode::Multiply;
|
||||
t.isolate = true;
|
||||
t.locked = true;
|
||||
t.name = Some("Title".into());
|
||||
t.appearance.effects = vec![shadow.clone()];
|
||||
}
|
||||
let opts = LoadOptions { text_as: vectorcraft_pdf::TextAs::Outlines, ..Default::default() };
|
||||
let (doc, _) = load::outlined_if_asked(doc, vec![], &opts);
|
||||
let mut outlines = vec![];
|
||||
doc.walk(|n| {
|
||||
if n.extras().type_outlines {
|
||||
outlines.push(n.clone());
|
||||
}
|
||||
});
|
||||
let [o] = &outlines[..] else { panic!("one group of outlines: {outlines:?}") };
|
||||
assert_eq!((o.opacity, o.blend, o.isolate), (0.5, vectorcraft_color::BlendMode::Multiply, true));
|
||||
assert_eq!(o.appearance.effects, [shadow]);
|
||||
assert!(o.locked && o.visible);
|
||||
assert_eq!(o.name.as_deref(), Some("Title"));
|
||||
}
|
||||
|
||||
/// Threaded type of the editing data, its story all in its first frame as the editing data gives
|
||||
/// it, outlined: each frame's outlines show the text that flows into it.
|
||||
#[test]
|
||||
fn outlined_threaded_type_of_the_editing_data_shows_the_text_of_every_frame() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 600, "height": 300})).unwrap();
|
||||
let story = "The quick brown fox jumps over the lazy dog and keeps running far beyond the first frame. ".repeat(4);
|
||||
let a =
|
||||
s.execute("text.create", &json!({"x": 10, "y": 10, "text": story, "area": {"width": 150, "height": 60}})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = s.execute("shape.rectangle", &json!({"x": 200, "y": 10, "width": 150, "height": 200})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a, b]})).unwrap();
|
||||
s.execute("text.thread.create", &json!({})).unwrap();
|
||||
// As the editing data gives it: the first frame holds the story, the second nothing yet.
|
||||
let mut doc = s.doc().unwrap().doc.as_ref().clone();
|
||||
let runs = |id: u64| match &doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.runs.clone(),
|
||||
_ => panic!("area type"),
|
||||
};
|
||||
let (shown, flowed) = (runs(a), runs(b));
|
||||
let empty: Vec<_> = flowed.iter().take(1).map(|r| vectorcraft_doc::TextRun { text: String::new(), ..r.clone() }).collect();
|
||||
for (id, put) in [(a, [shown, flowed].concat()), (b, empty)] {
|
||||
if let NodeKind::Text(t) = &mut doc.node_mut(NodeId(id)).unwrap().kind {
|
||||
t.runs = put;
|
||||
t.cached_bounds = None;
|
||||
t.cached_baselines.clear();
|
||||
}
|
||||
}
|
||||
let opts = LoadOptions { text_as: vectorcraft_pdf::TextAs::Outlines, ..Default::default() };
|
||||
let (doc, _) = load::outlined_if_asked(doc, vec![], &opts);
|
||||
let mut right = 0;
|
||||
doc.walk(|n| {
|
||||
if matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) && n.geometric_bounds().is_some_and(|r| r.x0 >= 190.0) {
|
||||
right += 1;
|
||||
}
|
||||
});
|
||||
assert!(right > 0, "the second frame's text is outlined where it flows");
|
||||
assert!(doc.text_threads.is_empty(), "outlined frames are in no thread: {:?}", doc.text_threads);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_data_false_opens_only_what_the_page_prints() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -48,7 +48,7 @@ mod options;
|
||||
mod presets;
|
||||
mod report;
|
||||
|
||||
pub(crate) use analysis::see_through_images;
|
||||
pub(crate) use analysis::{outlined_text, see_through_images};
|
||||
pub(crate) use apply::flatten_document;
|
||||
pub use commands::specs;
|
||||
pub use options::FlattenOptions;
|
||||
|
||||
@@ -51,7 +51,7 @@ impl Scratch {
|
||||
}
|
||||
|
||||
/// Type as outlines painted like it ([`effects::outline_text`]), keeping its transparency.
|
||||
pub(super) fn outlined_text(n: &Node) -> Option<Node> {
|
||||
pub(crate) fn outlined_text(n: &Node) -> Option<Node> {
|
||||
effects::outline_text(n).map(|g| Node { knockout: Knockout::Off, ..carry(n, g) })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user