mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 04:54:45 +00:00
Merge pull request #6133 from dotCooCoo/fix/ai-warped-type-compare
.ai import: warped type that draws differently from the file opens as its drawn art
This commit is contained in:
@@ -3288,6 +3288,40 @@ proptest! {
|
||||
survive("hostile .ai envelope", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?;
|
||||
}
|
||||
|
||||
/// A `.ai` whose warped type is damaged or hostile: any warp style and settings, on the object or
|
||||
/// on a fill, a look that reaches far away or holds type of its own, the text cut short.
|
||||
#[test]
|
||||
fn ai_warped_type_never_panics(
|
||||
style in prop::sample::select(vec![0i64, 1, 8, 15, 16, -3, i64::MAX]),
|
||||
bend in prop::sample::select(vec!["0.5", "0", "1", "-1", "2", "1e308", "-1e308", "nan"]),
|
||||
distort in prop::sample::select(vec!["0", "0.4", "-1", "1e308", "nan"]),
|
||||
reach in prop::sample::select(vec!["90", "16000", "1e7", "1e308", "-1e308", "nan"]),
|
||||
on_fill in any::<bool>(),
|
||||
look_type in any::<bool>(),
|
||||
objects in 1usize..4,
|
||||
cut in 0usize..3_000,
|
||||
edits in prop::collection::vec((0usize..3_000, prop::sample::select(vec!['0', '9', '-', ' ', '\n', '(', ')', '/', ':', ';', ',', '[', ']', '%', '_', 'X', 'u', 'U'])), 0..8),
|
||||
) {
|
||||
let basic = |name: &str, body: &str| format!("/BasicFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n{body}; /Dict ;\n /Part ,\n");
|
||||
let chain = |kids: &[String]| format!("/CompoundFilter :\n(Chain Style Filter) 0 0 /Filter ,\n1 /Visible ,\n{};\n /Part ,\n", kids.concat());
|
||||
let fill = basic("Fill Style Filter", "/FillStyle : 0 O\n0 1 1 0 1 0 0 Xa\n0 1 0 0 0 Xy\n0 1 Xd\n/Def ;\n (FillStyle) ,\n");
|
||||
let warp = basic("Adobe Deform", &format!("{style} /Int (DeformStyle) ,\n{bend} /Real (DeformValue) ,\n{distort} /Real (DeformHoriz) ,\n{distort} /Real (DeformVert) ,\n0 /Bool (Rotate) ,\n"));
|
||||
let root = if on_fill {
|
||||
let stack = format!("/CompoundFilter :\n(Stack Style Filter) 0 0 /Filter ,\n1 /Visible ,\n{}{};\n /Part ,\n", chain(&[warp, fill]), basic("Conduit Filter", ""));
|
||||
chain(&[stack])
|
||||
} else {
|
||||
chain(&[fill, warp])
|
||||
};
|
||||
let styles = format!("%AI9_BeginArtStyles\n/KnownStyle :\n(Warped) /Name ,\n/ActiveStyle :\n{root}%AI9_EndArtStyles\n");
|
||||
let slot = if look_type { "/AI11Text :\n1 /FreeUndo ,\n0 /FrameIndex ,\n0 /StoryIndex ,\n;\n" } else { "" };
|
||||
let object = format!(
|
||||
"u\n{slot}0 1 1 0 1 0 0 Xa\n10 10 m\n{reach} 10 L\n{reach} {reach} L\n10 90 L\nf\nU\n9 () XW\n%_/AI11Text :\n%_0 /FreeUndo ,\n%_0 /FrameIndex ,\n%_0 /StoryIndex ,\n%_;\n1 (Warped) XW\n"
|
||||
);
|
||||
let data = mutate_text(&text_editing_of(&vertical_document_text(), &(styles + &object.repeat(objects))), cut, &edits);
|
||||
let private = [b"%AI24_ZStandard_Data".as_slice(), &zstd(&data)].concat();
|
||||
survive("hostile .ai warped type", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, false, 1.0).0))?;
|
||||
}
|
||||
|
||||
/// A `.ai` whose editing data is damaged or hostile.
|
||||
#[test]
|
||||
fn ai_editing_data_never_panics(cut in 0usize..3_000, edits in prop::collection::vec((0usize..3_000, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '/', ':', ';', '[', ']', '%', 'q', 'Q', 'W', 'u', 'U', 'L', 'l', 'c', 'C', 'v', 'V', 'y', 'Y', 'k', 'x'])), 0..12), damage in prop::collection::vec((0usize..4_000, any::<u8>()), 0..4)) {
|
||||
|
||||
@@ -35,6 +35,9 @@ use super::lex::find;
|
||||
use crate::ps;
|
||||
|
||||
pub(crate) use envelope::DRAWN as ENVELOPE_DRAWN;
|
||||
#[cfg(test)]
|
||||
pub(crate) use envelope::MAX_CHECKED;
|
||||
pub(crate) use envelope::{WARPED_TYPE_DRAWN, WarpedType};
|
||||
pub(crate) use frames::text_frames;
|
||||
pub(crate) use paint::MAX_STOPS as MAX_GRADIENT_STOPS;
|
||||
pub use read::{
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
//! one-patch mesh. An envelope whose parts don't read, or that doesn't come out as drawn, keeps its
|
||||
//! drawn art.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use kurbo::{Affine, BezPath, Point, Rect, Vec2};
|
||||
@@ -31,7 +31,7 @@ use vectorcraft_doc::live::{
|
||||
EnvelopeKind, EnvelopeOptions, GradientMesh, H_DOWN, H_LEFT, H_RIGHT, H_UP, PreserveShape, default_fidelity, envelope_content_boxes,
|
||||
envelope_stroke_margin,
|
||||
};
|
||||
use vectorcraft_doc::{Document, ImageBlob, LayerColor, Node, NodeId, NodeKind, PatternDef};
|
||||
use vectorcraft_doc::{Appearance, Document, Effect, ImageBlob, LayerColor, Node, NodeId, NodeKind, PatternDef};
|
||||
use vectorcraft_geom::PathData;
|
||||
|
||||
use super::obj::Obj;
|
||||
@@ -41,10 +41,12 @@ use crate::import::native::compare::{MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, compa
|
||||
pub(super) const PLUGIN: &str = "Adobe Deform Plugin"; // brand-ok: the plugin name the files use
|
||||
/// The note for an envelope that keeps its drawn art.
|
||||
pub(crate) const DRAWN: &str = "an envelope VectorCraft couldn't read as a live envelope comes in as its drawn art";
|
||||
/// The note for warped type that keeps its drawn art, before how many.
|
||||
pub(crate) const WARPED_TYPE_DRAWN: &str = "warped text objects that came in as drawn art, not as editable type";
|
||||
/// The most envelopes waiting for their drawn art at once.
|
||||
pub(super) const MAX_PENDING: usize = 64;
|
||||
/// The most envelopes one import draws to check; more keep their drawn art.
|
||||
pub(super) const MAX_CHECKED: usize = 256;
|
||||
/// The most envelopes and warped text objects one import draws to check; more keep their drawn art.
|
||||
pub(crate) const MAX_CHECKED: usize = 256;
|
||||
/// The most rows or columns of a mesh read (Make with Mesh's own limit).
|
||||
const MAX_GRID: u32 = 50;
|
||||
/// How far, in points over width and height together, a saved size may lie from the contents'
|
||||
@@ -343,14 +345,107 @@ fn differing(live: &Document, drawn: &Document, region: Rect, scale: f64) -> Opt
|
||||
Some((any, solid, inked))
|
||||
}
|
||||
|
||||
/// The differences from their drawn art that the live envelopes of one import may add up to, as
|
||||
/// shares of the page drawn as the page check draws it: half of what that check allows
|
||||
/// ([`MAX_DIFFERENCE`], [`MAX_SOLID_DIFFERENCE`]), so live envelopes never make the file open from
|
||||
/// its PDF part instead.
|
||||
/// The differences from their drawn art that the live envelopes and warped type of one import may
|
||||
/// add up to, as shares of the page drawn as the page check draws it: half of what that check allows
|
||||
/// ([`MAX_DIFFERENCE`], [`MAX_SOLID_DIFFERENCE`]), so they never make the file open from its PDF part
|
||||
/// instead.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct Spent {
|
||||
any: f64,
|
||||
solid: f64,
|
||||
pub(crate) struct Spent {
|
||||
pub(super) any: f64,
|
||||
pub(super) solid: f64,
|
||||
}
|
||||
|
||||
/// A text object read with a Warp.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct WarpedLook {
|
||||
/// The id of its slot.
|
||||
pub(super) slot: NodeId,
|
||||
/// The drawn art its style's look saved.
|
||||
pub(super) look: Node,
|
||||
/// That art's box, when the comparisons with the page leave it out ([`super::Structure::restyled`]).
|
||||
pub(super) left_out: Option<Rect>,
|
||||
}
|
||||
|
||||
/// The text objects read with a Warp, and what the import has left of the budget for checks
|
||||
/// ([`drawn_alike`]). Type is laid out after the layers are read, so these wait for that
|
||||
/// ([`Self::settle`]).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WarpedType {
|
||||
pub(super) looks: Vec<WarpedLook>,
|
||||
pub(super) checks: usize,
|
||||
pub(super) spent: Spent,
|
||||
pub(super) page: Option<Rect>,
|
||||
}
|
||||
|
||||
impl WarpedType {
|
||||
/// Replace each warped text object of `doc` that doesn't draw as its saved look (or that the
|
||||
/// budget has no check left for) with that look: `made` gives the text object a slot became. A
|
||||
/// frame in `threaded` stays type, since the frames of a thread share one story. A replaced
|
||||
/// object's box goes back into the comparisons with the page (out of `restyled`), and only what
|
||||
/// `doc` shows counts against the page's share of the budget. Returns how many were replaced.
|
||||
pub(crate) fn settle(
|
||||
self,
|
||||
doc: &mut Document,
|
||||
made: impl Fn(NodeId) -> Option<NodeId>,
|
||||
threaded: &BTreeSet<NodeId>,
|
||||
restyled: &mut Vec<Rect>,
|
||||
) -> usize {
|
||||
let Self { looks, mut checks, mut spent, page } = self;
|
||||
let mut shown_ids = BTreeSet::new();
|
||||
doc.layers.iter().for_each(|l| collect_shown(l, &mut shown_ids));
|
||||
let mut replaced = 0;
|
||||
for WarpedLook { slot, mut look, left_out } in looks {
|
||||
let Some(id) = made(slot).filter(|id| !threaded.contains(id)) else { continue };
|
||||
let Some(text) = doc.node(id).filter(|n| matches!(n.kind, NodeKind::Text(_)) && warps(&n.appearance)).cloned() else { continue };
|
||||
let mut alike = false;
|
||||
if checks > 0 {
|
||||
checks -= 1;
|
||||
let layer = doc.alloc_id();
|
||||
let shared = Shared { patterns: &doc.patterns, images: &doc.images };
|
||||
let (mut shown, mut plain) = (text.clone(), look.clone());
|
||||
(shown.visible, shown.opacity) = (true, 1.0);
|
||||
(plain.visible, plain.opacity) = (true, 1.0);
|
||||
alike = warped_alike(&shown, &plain, layer, &shared, page.filter(|_| shown_ids.contains(&id)), &mut spent);
|
||||
}
|
||||
if alike {
|
||||
continue;
|
||||
}
|
||||
(look.id, look.name, look.visible, look.locked) = (id, text.name.clone(), text.visible, text.locked);
|
||||
if let Some(wrap) = text.extras().wrap {
|
||||
look.extras_mut().wrap = Some(wrap);
|
||||
}
|
||||
if let Some(n) = doc.node_mut(id) {
|
||||
*n = look;
|
||||
replaced += 1;
|
||||
if let Some(at) = left_out.and_then(|b| restyled.iter().position(|r| *r == b)) {
|
||||
restyled.remove(at);
|
||||
}
|
||||
}
|
||||
}
|
||||
replaced
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl WarpedType {
|
||||
/// The text objects of `looks` (slot and saved look) with no check left.
|
||||
pub(crate) fn unchecked(looks: Vec<(NodeId, Node)>) -> Self {
|
||||
Self { looks: looks.into_iter().map(|(slot, look)| WarpedLook { slot, look, left_out: None }).collect(), ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Add to `out` the ids of `n` and what is in it, unless hidden.
|
||||
fn collect_shown(n: &Node, out: &mut BTreeSet<NodeId>) {
|
||||
if n.visible {
|
||||
out.insert(n.id);
|
||||
n.children().into_iter().flatten().for_each(|c| collect_shown(c, out));
|
||||
}
|
||||
}
|
||||
|
||||
/// Does `appearance` have a Warp that shows, on the object or on one of its fills or strokes?
|
||||
pub(super) fn warps(appearance: &Appearance) -> bool {
|
||||
let warp = |e: &Effect| e.visible && e.id.starts_with("warp.");
|
||||
appearance.effects.iter().any(warp) || appearance.items.iter().any(|i| i.visible() && i.effects().iter().any(warp))
|
||||
}
|
||||
|
||||
/// Do `live` and `drawn` draw alike: few of the pixels either inks differ, and fewer still where a
|
||||
@@ -358,7 +453,19 @@ pub(super) struct Spent {
|
||||
/// with the differences in `spent` the page check still takes the layers' art (it is added to
|
||||
/// `spent` when they do)? Both are drawn on `layer`, a layer id no other node has.
|
||||
pub(super) fn drawn_alike(live: &Node, drawn: &Node, layer: NodeId, shared: &Shared<'_>, page: Option<Rect>, spent: &mut Spent) -> bool {
|
||||
let (Some(a), Some(b)) = (live.visual_bounds(), drawn.visual_bounds()) else { return false };
|
||||
alike_within(live, drawn, live.visual_bounds(), layer, shared, page, spent)
|
||||
}
|
||||
|
||||
/// [`drawn_alike`] for type under a Warp, compared over all that its effects draw, which can reach
|
||||
/// past its own box.
|
||||
pub(super) fn warped_alike(live: &Node, drawn: &Node, layer: NodeId, shared: &Shared<'_>, page: Option<Rect>, spent: &mut Spent) -> bool {
|
||||
let reach = vectorcraft_render::effect_bounds(&alone(live, layer, shared).symbol_boxes(), live);
|
||||
alike_within(live, drawn, vectorcraft_geom::union_opt(live.visual_bounds(), reach), layer, shared, page, spent)
|
||||
}
|
||||
|
||||
/// [`drawn_alike`], with `live` measured as `reach`.
|
||||
fn alike_within(live: &Node, drawn: &Node, reach: Option<Rect>, layer: NodeId, shared: &Shared<'_>, page: Option<Rect>, spent: &mut Spent) -> bool {
|
||||
let (Some(a), Some(b)) = (reach, drawn.visual_bounds()) else { return false };
|
||||
let region = a.union(b).inflate(2.0, 2.0);
|
||||
if !(region.is_finite() && region.width() > 0.0 && region.height() > 0.0) || region.width().max(region.height()) > 1e6 {
|
||||
return false;
|
||||
|
||||
@@ -183,6 +183,8 @@ pub struct Structure {
|
||||
/// The live envelopes read from the editing data, by id, with the drawn art each took the place
|
||||
/// of: the importer puts that art back when the layers with the envelopes don't draw as the page.
|
||||
pub envelopes_drawn: Vec<(NodeId, Node)>,
|
||||
/// The warped text objects, checked against their drawn looks once their type is laid out.
|
||||
pub warped_type: super::envelope::WarpedType,
|
||||
/// The document's graphic styles are those of the file's style list, in place of the defaults.
|
||||
pub styles_read: bool,
|
||||
/// The file's `%AI24_LargeCanvasScale` (1 when it declares none), applied or not.
|
||||
@@ -495,10 +497,13 @@ struct Reader<'a> {
|
||||
envelope_art: Option<Node>,
|
||||
/// Envelopes waiting for their drawn art, innermost last.
|
||||
envelopes: Vec<PendingEnvelope>,
|
||||
/// How many more envelopes this import may draw to check ([`super::envelope::drawn_alike`]).
|
||||
/// How many more envelopes and warped text objects this import may draw to check
|
||||
/// ([`super::envelope::drawn_alike`]).
|
||||
envelope_checks: usize,
|
||||
/// How far the live envelopes read so far draw from their drawn art ([`super::envelope::Spent`]).
|
||||
envelope_spent: super::envelope::Spent,
|
||||
/// The text objects read with a Warp ([`super::envelope::WarpedType`]).
|
||||
warped_type: Vec<super::envelope::WarpedLook>,
|
||||
/// The live envelopes made so far and the drawn art each took the place of
|
||||
/// ([`Structure::envelopes_drawn`]).
|
||||
envelopes_drawn: Vec<(NodeId, Node)>,
|
||||
@@ -700,6 +705,7 @@ impl<'a> Reader<'a> {
|
||||
envelopes: Vec::new(),
|
||||
envelope_checks: super::envelope::MAX_CHECKED,
|
||||
envelope_spent: super::envelope::Spent::default(),
|
||||
warped_type: Vec::new(),
|
||||
envelopes_drawn: Vec::new(),
|
||||
clip_fills: HashSet::new(),
|
||||
looks: HashSet::new(),
|
||||
|
||||
@@ -1032,6 +1032,8 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
let to_doc = self.ensure_space();
|
||||
let artboard = self.first_board();
|
||||
// Before the artboards are taken below: the page is the first artboard's.
|
||||
let page = self.page_in_doc();
|
||||
self.check_symbols();
|
||||
self.order_symbols();
|
||||
drop_commented_copies(&mut self.doc.layers, &self.hidden_ids);
|
||||
@@ -1139,6 +1141,9 @@ impl<'a> Reader<'a> {
|
||||
for a in brushes.iter_mut().flat_map(|(lib, _)| lib.iter_mut().flat_map(|b| b.art_mut())) {
|
||||
vectorcraft_doc::swatches::map_node_solid_paints(a, &mut follow);
|
||||
}
|
||||
for w in &mut self.warped_type {
|
||||
vectorcraft_doc::swatches::map_node_solid_paints(&mut w.look, &mut follow);
|
||||
}
|
||||
}
|
||||
let mut warnings = self.warnings;
|
||||
if let Some((lib, unread)) = brushes {
|
||||
@@ -1179,6 +1184,12 @@ impl<'a> Reader<'a> {
|
||||
placeholders: self.placeholders,
|
||||
placed_elsewhere: self.placed_elsewhere,
|
||||
envelopes_drawn: self.envelopes_drawn,
|
||||
warped_type: super::super::envelope::WarpedType {
|
||||
looks: self.warped_type,
|
||||
checks: self.envelope_checks,
|
||||
spent: self.envelope_spent,
|
||||
page,
|
||||
},
|
||||
styles_read,
|
||||
canvas_scale: self.canvas_scale,
|
||||
xml_ids: self.xml_ids,
|
||||
|
||||
@@ -706,6 +706,7 @@ impl<'a> Reader<'a> {
|
||||
self.looks.insert(look.id);
|
||||
}
|
||||
// The type that a text object's look draws is the object's own, whatever comes of its style.
|
||||
let mut look_drew_type = false;
|
||||
if after
|
||||
&& let Some(f) = self.frames.last_mut()
|
||||
&& let [.., look, object] = f.children.as_mut_slice()
|
||||
@@ -715,7 +716,7 @@ impl<'a> Reader<'a> {
|
||||
&& self.hidden_ids.contains(&look.id) == hidden
|
||||
&& matches!(look.kind, NodeKind::Group { clip: false, .. })
|
||||
{
|
||||
strip_look_type(look);
|
||||
look_drew_type = strip_look_type(look);
|
||||
}
|
||||
if code != 1.0 || style.is_empty() || !after {
|
||||
return;
|
||||
@@ -761,9 +762,25 @@ impl<'a> Reader<'a> {
|
||||
}
|
||||
let Some(mut object) = f.children.pop() else { return };
|
||||
let Some(look) = f.children.pop() else { return };
|
||||
if let Some(b) = look.visual_bounds().filter(|_| !hidden && object.visible && look.visible) {
|
||||
let left_out = look.visual_bounds().filter(|_| !hidden && object.visible && look.visible);
|
||||
if let Some(b) = left_out {
|
||||
self.restyled.push(b);
|
||||
}
|
||||
// Warped type is checked against its look once it is laid out. The look stands in for
|
||||
// the type only when it holds all that the object draws: not when it draws nothing or
|
||||
// draws type of its own.
|
||||
if slot_of(object.name.as_deref()).is_some() && crate::import::ai::envelope::warps(&appearance) && self.warped_type.len() < MAX_NODES {
|
||||
let mut saved = look.clone();
|
||||
// When the object itself is on `%_` lines, every child of its look is too, so the
|
||||
// look's own copies can't be told apart from the rest.
|
||||
if !hidden {
|
||||
strip(&mut saved, &self.hidden_ids);
|
||||
}
|
||||
if !look_drew_type && !holds_type(&saved) && saved.visual_bounds().is_some() && draws_art(&saved, 0) {
|
||||
take_attributes(&mut saved, &object);
|
||||
self.warped_type.push(crate::import::ai::envelope::WarpedLook { slot: object.id, look: saved, left_out });
|
||||
}
|
||||
}
|
||||
object.appearance = appearance.into();
|
||||
// The object's transparency is written on its look, after the look's group ends.
|
||||
(object.opacity, object.blend, object.isolate, object.knockout, object.knockout_shape) =
|
||||
@@ -814,29 +831,47 @@ impl<'a> Reader<'a> {
|
||||
look.name = object.name.clone().or(look.name.take());
|
||||
look.visible = object.visible;
|
||||
look.locked = object.locked;
|
||||
// The object's note, Show Center, URL and data (object tags) stay with the look that stands
|
||||
// for it, in place of the look's own where the object has them.
|
||||
if let Some(a) = object.extras().attrs.as_deref().cloned() {
|
||||
look.edit_attrs(|l| {
|
||||
if a.show_center.is_some() {
|
||||
l.show_center = a.show_center;
|
||||
}
|
||||
if !a.url.is_empty() {
|
||||
l.url = a.url;
|
||||
}
|
||||
if !a.note.is_empty() {
|
||||
l.note = a.note;
|
||||
}
|
||||
let room = MAX_TAGS.saturating_sub(l.data.len());
|
||||
l.data.extend(a.data.into_iter().take(room));
|
||||
});
|
||||
}
|
||||
take_attributes(look, &object);
|
||||
if !hidden {
|
||||
self.drawn_looks += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Does `n` or anything shown in it (to [`MAX_DEPTH`]) paint? Any object other than a path, a
|
||||
/// compound path or a group counts: it draws more than its fills and strokes.
|
||||
fn draws_art(n: &Node, depth: usize) -> bool {
|
||||
depth > MAX_DEPTH
|
||||
|| !matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Group { .. })
|
||||
|| n.appearance.items.iter().any(|i| i.visible() && !i.paint().is_none())
|
||||
|| n.children().into_iter().flatten().any(|c| c.visible && draws_art(c, depth + 1))
|
||||
}
|
||||
|
||||
/// Is `n` or anything in it a text slot?
|
||||
fn holds_type(n: &Node) -> bool {
|
||||
slot_of(n.name.as_deref()).is_some() || n.children().into_iter().flatten().any(|c| holds_type(c))
|
||||
}
|
||||
|
||||
/// The note, Show Center, URL and data (object tags) of `object` stay with the `look` that stands
|
||||
/// for it, in place of the look's own where the object has them.
|
||||
fn take_attributes(look: &mut Node, object: &Node) {
|
||||
if let Some(a) = object.extras().attrs.as_deref().cloned() {
|
||||
look.edit_attrs(|l| {
|
||||
if a.show_center.is_some() {
|
||||
l.show_center = a.show_center;
|
||||
}
|
||||
if !a.url.is_empty() {
|
||||
l.url = a.url;
|
||||
}
|
||||
if !a.note.is_empty() {
|
||||
l.note = a.note;
|
||||
}
|
||||
let room = MAX_TAGS.saturating_sub(l.data.len());
|
||||
l.data.extend(a.data.into_iter().take(room));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `look`'s paths don't all paint the same.
|
||||
pub(super) fn paints_apart(look: &Node) -> bool {
|
||||
fn leaves<'a>(n: &'a Node, out: &mut Vec<&'a Node>) {
|
||||
@@ -1138,14 +1173,19 @@ fn with_look(layer: &mut AppearanceItem, look: &crate::import::ai::style::ItemLo
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove from `n` the text slots of the type its look draws ([`super::is_look_slot`]), in its groups too.
|
||||
fn strip_look_type(n: &mut Node) {
|
||||
/// Remove from `n` the text slots of the type its look draws ([`super::is_look_slot`]), in its groups
|
||||
/// too. Returns whether there were any.
|
||||
fn strip_look_type(n: &mut Node) -> bool {
|
||||
let mut any = false;
|
||||
if let Some(children) = n.children_mut() {
|
||||
let before = children.len();
|
||||
children.retain(|c| !super::is_look_slot(c.name.as_deref()));
|
||||
any = children.len() != before;
|
||||
for c in children.iter_mut().filter(|c| c.children().is_some_and(|k| !k.is_empty())) {
|
||||
strip_look_type(Arc::make_mut(c));
|
||||
any |= strip_look_type(Arc::make_mut(c));
|
||||
}
|
||||
}
|
||||
any
|
||||
}
|
||||
|
||||
/// Remove from `n` the children read from `%_` lines (`hidden`: the object itself, not its look).
|
||||
|
||||
@@ -6847,6 +6847,134 @@ fn an_envelope_that_doesnt_come_out_as_drawn_keeps_its_drawn_art() {
|
||||
assert_eq!(r.warnings, [super::envelope::DRAWN]);
|
||||
}
|
||||
|
||||
/// A document of `n` warped text objects, with the art the live warp makes of each, in its look.
|
||||
fn warped_type_doc(n: usize) -> (Document, Vec<super::envelope::WarpedLook>) {
|
||||
use vectorcraft_doc::{Appearance, Effect, TextObject};
|
||||
let mut doc = Document::new(400.0, 200.0);
|
||||
let mut looks = vec![];
|
||||
let mut layer = Node::layer(doc.alloc_id(), "Art", vectorcraft_doc::LayerColor::Preset(0));
|
||||
for i in 0..n {
|
||||
let t = TextObject::point(kurbo::Point::new(20.0, 40.0 + 40.0 * i as f64), "TILT", Default::default());
|
||||
let mut text = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
let mut appearance = Appearance::default();
|
||||
appearance.effects.push(Effect { id: "warp.arc".into(), params: serde_json::json!({"bend": 40.0}), visible: true });
|
||||
text.appearance = appearance.into();
|
||||
text.name = Some(format!("Type {i}"));
|
||||
text.locked = i == 0;
|
||||
text.visible = i != 0;
|
||||
let mut look = vectorcraft_effects::expand_outlined(None, &text, None).unwrap();
|
||||
look.id = doc.alloc_id();
|
||||
looks.push(super::envelope::WarpedLook { slot: text.id, look, left_out: None });
|
||||
layer.children_mut().unwrap().push(Arc::new(text));
|
||||
}
|
||||
doc.layers = vec![Arc::new(layer)];
|
||||
(doc, looks)
|
||||
}
|
||||
|
||||
/// Warped text objects share the import's budget of checks. One past it keeps its drawn art with its
|
||||
/// text object's name and state, as one that fails the check does, and its box and wrap setting.
|
||||
#[test]
|
||||
fn warped_type_past_the_budget_comes_in_as_its_look() {
|
||||
let (mut doc, mut looks) = warped_type_doc(2);
|
||||
let (first, second) = (looks[0].slot, looks[1].slot);
|
||||
let left_out = Rect::new(1.0, 2.0, 3.0, 4.0);
|
||||
looks[1].left_out = Some(left_out);
|
||||
let wrap = vectorcraft_doc::TextWrap::default();
|
||||
doc.node_mut(second).unwrap().extras_mut().wrap = Some(wrap);
|
||||
let warped = super::envelope::WarpedType { looks, checks: 1, ..Default::default() };
|
||||
let mut restyled = vec![Rect::new(5.0, 6.0, 7.0, 8.0), left_out];
|
||||
assert_eq!(warped.settle(&mut doc, Some, &Default::default(), &mut restyled), 1);
|
||||
assert_eq!(restyled, [Rect::new(5.0, 6.0, 7.0, 8.0)]);
|
||||
assert!(matches!(doc.node(first).unwrap().kind, NodeKind::Text(_)));
|
||||
let kept = doc.node(second).unwrap();
|
||||
assert!(matches!(kept.kind, NodeKind::Group { .. } | NodeKind::Path { .. }), "{kept:#?}");
|
||||
assert_eq!((kept.name.as_deref(), kept.visible, kept.locked), (Some("Type 1"), true, false));
|
||||
assert_eq!(kept.extras().wrap, Some(wrap));
|
||||
}
|
||||
|
||||
/// The budget for warped type is what the envelopes left: their checks and the page's share they
|
||||
/// spent, with the page they are held to.
|
||||
#[test]
|
||||
fn warped_type_gets_what_the_envelopes_left_of_the_budget() {
|
||||
let flat = vectorcraft_doc::live::EnvelopeKind::Mesh {
|
||||
rows: 1,
|
||||
cols: 1,
|
||||
points: vec![Point::new(40.0, 20.0), Point::new(160.0, 20.0), Point::new(40.0, 80.0), Point::new(160.0, 80.0)],
|
||||
handles: vec![],
|
||||
};
|
||||
let exact = read_ok(&layer(&envelope_of(&straight_mesh(1, 1, (120.0, 60.0)), ENVELOPE_KEYS, &envelope_content(), 2, &drawn_content(&flat))));
|
||||
let beside = format!("{RED}{}f\n", rect_path(40.5, 20.0, 160.5, 80.0));
|
||||
let shifted = read_ok(&layer(&envelope_of(&straight_mesh(1, 1, (120.0, 60.0)), ENVELOPE_KEYS, &envelope_content(), 2, &beside)));
|
||||
for r in [&exact, &shifted] {
|
||||
assert_eq!(r.warped_type.checks, super::envelope::MAX_CHECKED - 1);
|
||||
assert!(r.warped_type.page.is_some());
|
||||
}
|
||||
live_envelope(&art(&shifted.doc)[0]);
|
||||
assert!(shifted.warped_type.spent.any > exact.warped_type.spent.any);
|
||||
}
|
||||
|
||||
/// In a file with an artboard list, warped type is held to the first artboard, not to the crop marks.
|
||||
#[test]
|
||||
fn warped_type_is_held_to_the_first_artboard() {
|
||||
let boards = "%_/Document :\n%_/Dictionary :\n%_/Array :\n\
|
||||
%_/Dictionary :\n%_0 200 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_300 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_; ,\n\
|
||||
%_/Dictionary :\n%_400 0 /RealPointRelToROrigin\n%_ (PositionPoint2) ,\n%_600 -150 /RealPointRelToROrigin\n%_ (PositionPoint1) ,\n%_; ,\n\
|
||||
%_; (ArtboardArray) ,\n%_; /NotRecorded ,\n%_;\n";
|
||||
let r = read_ok(&format!("{boards}{}", layer(&format!("{RED}{}f\n", rect_path(20.0, 20.0, 40.0, 40.0)))));
|
||||
assert_eq!(r.doc.artboards[0].rect, Rect::new(0.0, 0.0, 300.0, 200.0));
|
||||
assert_eq!(r.warped_type.page, Some(Rect::new(0.0, 0.0, 300.0, 200.0)));
|
||||
}
|
||||
|
||||
/// Type whose Warp is hidden draws no warp, so it stays type even with no check left.
|
||||
#[test]
|
||||
fn type_with_a_hidden_warp_stays_type() {
|
||||
let (mut doc, looks) = warped_type_doc(1);
|
||||
let slot = looks[0].slot;
|
||||
let mut appearance = (*doc.node(slot).unwrap().appearance).clone();
|
||||
appearance.effects[0].visible = false;
|
||||
doc.node_mut(slot).unwrap().appearance = appearance.into();
|
||||
let warped = super::envelope::WarpedType { looks, checks: 0, ..Default::default() };
|
||||
assert_eq!(warped.settle(&mut doc, Some, &Default::default(), &mut vec![]), 0);
|
||||
assert!(matches!(doc.node(slot).unwrap().kind, NodeKind::Text(_)));
|
||||
}
|
||||
|
||||
/// A warped frame of threaded type stays type, whatever its look draws: the thread's frames share
|
||||
/// one story. So does a slot that became no text object.
|
||||
#[test]
|
||||
fn warped_type_in_a_thread_stays_type() {
|
||||
let (mut doc, looks) = warped_type_doc(2);
|
||||
let (first, second) = (looks[0].slot, looks[1].slot);
|
||||
let warped = super::envelope::WarpedType { looks, ..Default::default() };
|
||||
assert_eq!(warped.settle(&mut doc, |id| (id != second).then_some(id), &[first].into(), &mut vec![]), 0);
|
||||
assert!(matches!(doc.node(first).unwrap().kind, NodeKind::Text(_)));
|
||||
assert!(matches!(doc.node(second).unwrap().kind, NodeKind::Text(_)));
|
||||
}
|
||||
|
||||
/// What is left of the page's share of the budget decides, and the page's share counts only for
|
||||
/// what the document shows.
|
||||
#[test]
|
||||
fn warped_type_on_the_page_spends_what_is_left_of_the_budget() {
|
||||
use crate::import::native::compare::{MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE};
|
||||
let (doc, mut looks) = warped_type_doc(2);
|
||||
// The second object shows; the first is hidden. Each look draws a little more than its object.
|
||||
let mark = read_ok(&layer(&format!("{RED}{}f\n", rect_path(2.0, 2.0, 2.6, 2.6))));
|
||||
let mark = art(&mark.doc)[0].clone();
|
||||
for (i, w) in looks.iter_mut().enumerate() {
|
||||
w.look.visible = true;
|
||||
w.look = Node::group(vectorcraft_doc::NodeId(u64::MAX - 10 - i as u64), vec![Arc::new(w.look.clone()), mark.clone()]);
|
||||
}
|
||||
let copy = |w: &super::envelope::WarpedLook| super::envelope::WarpedLook { slot: w.slot, look: w.look.clone(), left_out: None };
|
||||
let page = Some(Rect::new(0.0, 60.0, 60.0, 100.0));
|
||||
let settle = |w: &super::envelope::WarpedLook, spent| {
|
||||
let warped = super::envelope::WarpedType { looks: vec![copy(w)], checks: 1, spent, page };
|
||||
warped.settle(&mut doc.clone(), Some, &Default::default(), &mut vec![])
|
||||
};
|
||||
let spent = || super::envelope::Spent { any: MAX_DIFFERENCE / 2.0, solid: MAX_SOLID_DIFFERENCE / 2.0 };
|
||||
assert_eq!(settle(&looks[1], Default::default()), 0);
|
||||
assert_eq!(settle(&looks[1], spent()), 1);
|
||||
assert_eq!(settle(&looks[0], spent()), 0);
|
||||
}
|
||||
|
||||
/// An envelope that comes out close to its drawn art, but with more of the page differing than the
|
||||
/// page check can take from the live envelopes (its drawn art 2 pt to the side on a 200 × 100
|
||||
/// page), keeps the drawn art, so the file still opens from its layers.
|
||||
|
||||
@@ -209,9 +209,10 @@ pub(super) fn build_from(
|
||||
xml_ids,
|
||||
bindings,
|
||||
bindings_skipped,
|
||||
warped_type,
|
||||
warnings,
|
||||
} = structure;
|
||||
let restyled = left_out_of_comparisons(page, restyled, placeholders);
|
||||
let mut restyled = left_out_of_comparisons(page, restyled, placeholders);
|
||||
// Editing data in the units of the page's user space ([`in_units`]), or at its declared large
|
||||
// canvas scale ([`page_scale`]), is compared with the page scaled down to those units, and the
|
||||
// document read from it is scaled up by `unit`.
|
||||
@@ -416,6 +417,8 @@ pub(super) fn build_from(
|
||||
}
|
||||
// Area type flows around the wrap objects above it before it is compared with the page.
|
||||
resolve_text_wrap(&mut doc)?;
|
||||
let bound_text = text_bound_slots(&doc, &bindings, &xml_ids);
|
||||
settle_warped_type(warped_type, &mut doc, &filled, &threaded_frames, &bound_text, &mut restyled, &mut notes)?;
|
||||
if empty > 0 {
|
||||
notes.push(format!("{empty} {}", if frame.is_some() { TEXT_LEFT_OUT } else { TYPE_LEFT_OUT }));
|
||||
}
|
||||
@@ -555,6 +558,42 @@ fn bind_variables(
|
||||
notes.extend(ai::bindings_note(skipped));
|
||||
}
|
||||
|
||||
/// The text object slots of `doc` that `bindings` binds to a text variable, each named by its XML id
|
||||
/// (`ids`).
|
||||
fn text_bound_slots(doc: &Document, bindings: &[ai::BindingRef], ids: &std::collections::HashMap<String, Option<NodeId>>) -> BTreeSet<NodeId> {
|
||||
bindings
|
||||
.iter()
|
||||
.filter(|b| doc.variables.variable(&b.variable).is_some_and(|v| v.kind == VariableKind::Text))
|
||||
.filter_map(|b| b.target.as_ref().and_then(|t| ids.get(t).copied().flatten()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Warped type stays type where its live warp draws as its look, and where a text variable binds it
|
||||
/// (`bound_text`, by slot). Elsewhere the look takes its place, and one note says how many did.
|
||||
/// `filled` says what each slot became, and the comparisons with the page leave out the boxes in
|
||||
/// `restyled`. Fails as [`resolve_text_wrap`] does when the looks give the wrap shapes too many
|
||||
/// anchors.
|
||||
fn settle_warped_type(
|
||||
warped: ai::WarpedType,
|
||||
doc: &mut Document,
|
||||
filled: &std::collections::HashMap<NodeId, Filled>,
|
||||
threaded: &BTreeSet<NodeId>,
|
||||
bound_text: &BTreeSet<NodeId>,
|
||||
restyled: &mut Vec<Rect>,
|
||||
notes: &mut Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
let made = |slot| match filled.get(&slot).map(|f| f.text.as_slice()).filter(|_| !bound_text.contains(&slot)) {
|
||||
Some(&[one]) => Some(one),
|
||||
_ => None,
|
||||
};
|
||||
let replaced = warped.settle(doc, made, threaded, restyled);
|
||||
if replaced > 0 {
|
||||
notes.push(format!("{}: {replaced}", ai::WARPED_TYPE_DRAWN));
|
||||
resolve_text_wrap(doc)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Most anchors copied into the wrap shapes of a file's area type ([`vectorcraft_doc::text::wrap_work`]).
|
||||
const MAX_WRAP_WORK: usize = 2_000_000;
|
||||
|
||||
@@ -1881,6 +1920,34 @@ mod candidate_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Warped type whose look takes its place, with the type's wrap setting, is held to the same
|
||||
/// limit on wrap shapes: a file whose looks would copy more than `MAX_WRAP_WORK` anchors isn't
|
||||
/// read from its layers.
|
||||
#[test]
|
||||
fn warped_type_replaced_by_its_look_is_held_to_the_wrap_limit() {
|
||||
for (texts, ok) in [(150, true), (250, false)] {
|
||||
let mut doc = Document::new(100.0, 100.0);
|
||||
(0..texts).for_each(|_| _ = insert_area(&mut doc, 0, Rect::new(0.0, 0.0, 100.0, 100.0)));
|
||||
let t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(50.0, 50.0), "TILT", Default::default());
|
||||
let mut text = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
let mut appearance = vectorcraft_doc::Appearance::default();
|
||||
appearance.effects.push(vectorcraft_doc::Effect { id: "warp.arc".into(), params: serde_json::json!({"bend": 40.0}), visible: true });
|
||||
text.appearance = appearance.into();
|
||||
text.extras_mut().wrap = Some(vectorcraft_doc::TextWrap::default());
|
||||
let top = doc.layers[0].children().map_or(0, Vec::len);
|
||||
let id = doc.insert(Some(doc.layers[0].id), top, text).unwrap();
|
||||
let look = insert_wrap(&mut doc, Rect::new(40.0, 0.0, 60.0, 100.0), 10_000);
|
||||
let look = doc.remove(look).unwrap();
|
||||
assert!(resolve_text_wrap(&mut doc).is_ok());
|
||||
let warped = ai::WarpedType::unchecked(vec![(id, (*look).clone())]);
|
||||
let filled = std::collections::HashMap::from([(id, Filled { text: vec![id], objects: vec![id] })]);
|
||||
let mut notes = vec![];
|
||||
let settled = settle_warped_type(warped, &mut doc, &filled, &BTreeSet::new(), &BTreeSet::new(), &mut vec![], &mut notes);
|
||||
assert_eq!(settled.is_ok(), ok, "{texts} text objects");
|
||||
assert_eq!(notes, ["warped text objects that came in as drawn art, not as editable type: 1"]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Area type that wrapped around an object taken out of the layers flows without it.
|
||||
#[test]
|
||||
fn wrap_shapes_go_with_their_wrap_object() {
|
||||
|
||||
@@ -29,6 +29,7 @@ pub fn is_loss(note: &str) -> bool {
|
||||
note.ends_with(TEXT_LEFT_OUT)
|
||||
|| note.ends_with(TYPE_LEFT_OUT)
|
||||
|| note.ends_with("they came in as the page's art")
|
||||
|| note.starts_with(ai::WARPED_TYPE_DRAWN)
|
||||
|| note == PAGE_TYPE_LEFT_OUT
|
||||
|| note == PAGE_TYPE_UNCHECKED
|
||||
|| [ART_LEFT_OUT, LAYERS_UNREAD, DREW_DIFFERENTLY, crate::import::graphics::TOO_MUCH, crate::import::graphics::FAR_AWAY]
|
||||
|
||||
@@ -4794,6 +4794,334 @@ fn type_with_a_stacked_art_style_comes_in_as_type_with_that_appearance() {
|
||||
check(&r.document, &r.warnings);
|
||||
}
|
||||
|
||||
/// Where [`warped_style`] puts its Warp.
|
||||
#[derive(Clone, Copy)]
|
||||
enum WarpOn {
|
||||
Object,
|
||||
Fill,
|
||||
Stroke,
|
||||
}
|
||||
|
||||
/// An art style section with style `Warped`: a red fill with Effect › Warp › Arc at `bend` percent on
|
||||
/// `on`.
|
||||
fn warped_style(bend: f64, on: WarpOn) -> String {
|
||||
let basic = |name: &str, body: &str| {
|
||||
format!("/BasicFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n/Dictionary : /NotRecorded ,\n{body}; /Dict ;\n /Part ,\n")
|
||||
};
|
||||
let compound = |name: &str, kids: &[String]| format!("/CompoundFilter :\n({name}) 0 0 /Filter ,\n1 /Visible ,\n{};\n /Part ,\n", kids.concat());
|
||||
let fill = basic("Fill Style Filter", "/FillStyle : 0 O\n0 1 1 0 1 0 0 Xa\n0 1 0 0 0 Xy\n0 1 Xd\n/Def ;\n (FillStyle) ,\n");
|
||||
let warp = basic(
|
||||
"Adobe Deform",
|
||||
&format!(
|
||||
"1 /Int (DeformStyle) ,\n{} /Real (DeformValue) ,\n0 /Real (DeformHoriz) ,\n0 /Real (DeformVert) ,\n0 /Bool (Rotate) ,\n",
|
||||
bend / 100.0
|
||||
),
|
||||
);
|
||||
let stroke = || {
|
||||
basic("Stroke Style Filter", "/StrokeStyle : 0 1 0 0 0 Xy\n0 J 0 j 2 w 10 M []0 d0 XR\n0 0 0 1 0 0 0 XA\n1 0 Xd\n/Def ;\n (StrokeStyle) ,\n")
|
||||
};
|
||||
let conduit = || basic("Conduit Filter", "");
|
||||
let chain = match on {
|
||||
WarpOn::Object => compound("Chain Style Filter", &[fill, warp]),
|
||||
WarpOn::Fill => {
|
||||
compound("Chain Style Filter", &[compound("Stack Style Filter", &[compound("Chain Style Filter", &[warp, fill]), conduit()])])
|
||||
}
|
||||
WarpOn::Stroke => {
|
||||
compound("Chain Style Filter", &[compound("Stack Style Filter", &[compound("Chain Style Filter", &[warp, stroke()]), fill, conduit()])])
|
||||
}
|
||||
};
|
||||
format!("%AI9_BeginArtStyles\n/KnownStyle :\n(Warped) /Name ,\n/ActiveStyle :\n{chain}%AI9_EndArtStyles\n")
|
||||
}
|
||||
|
||||
/// `outline`, a path in the document's coordinates, as filled paths in a 100 × 100 file's art space,
|
||||
/// in the red of [`warped_style`].
|
||||
fn drawn_as_operators(outline: &kurbo::BezPath) -> String {
|
||||
use kurbo::PathEl;
|
||||
let mut out = String::from("0 1 1 0 1 0 0 Xa\n");
|
||||
let y = |v: f64| 100.0 - v;
|
||||
let mut last = kurbo::Point::ZERO;
|
||||
for el in outline.elements() {
|
||||
match *el {
|
||||
PathEl::MoveTo(p) => out += &format!("{} {} m\n", p.x, y(p.y)),
|
||||
PathEl::LineTo(p) => out += &format!("{} {} L\n", p.x, y(p.y)),
|
||||
PathEl::QuadTo(c, p) => {
|
||||
let (a, b) = (last + (c - last) * (2.0 / 3.0), p + (c - p) * (2.0 / 3.0));
|
||||
out += &format!("{} {} {} {} {} {} C\n", a.x, y(a.y), b.x, y(b.y), p.x, y(p.y));
|
||||
}
|
||||
PathEl::CurveTo(a, b, p) => out += &format!("{} {} {} {} {} {} C\n", a.x, y(a.y), b.x, y(b.y), p.x, y(p.y)),
|
||||
PathEl::ClosePath => out += "f\n",
|
||||
}
|
||||
last = match *el {
|
||||
PathEl::MoveTo(p) | PathEl::LineTo(p) | PathEl::QuadTo(_, p) | PathEl::CurveTo(_, _, p) => p,
|
||||
PathEl::ClosePath => last,
|
||||
};
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// What `n` holds as art, its paths joined, in the document's coordinates.
|
||||
fn outlines_of(n: &Node) -> kurbo::BezPath {
|
||||
let mut out = kurbo::BezPath::new();
|
||||
if let NodeKind::Path { path, .. } = &n.kind {
|
||||
out.extend(path.to_bezpath());
|
||||
}
|
||||
for c in n.children().into_iter().flatten() {
|
||||
out.extend(outlines_of(c));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The editing copy of `objects` text objects of `text` under style `style`, each with the look
|
||||
/// `drawing`.
|
||||
fn warped_file(style: &str, text: &str, objects: u32, drawing: &str) -> String {
|
||||
let stories: Vec<_> = (0..objects).map(|_| (text, 0u8, (8200.0, 8180.0), (0.0, 0.0), 0.0)).collect();
|
||||
let doc = text_document(&stories);
|
||||
let art: String = (0..objects)
|
||||
.map(|n| {
|
||||
let look = format!("u\n{drawing}\nU\n9 () XW\n");
|
||||
let own: String = text_object(n).lines().map(|l| format!("%_{l}\n")).collect();
|
||||
format!("{look}{own}1 (Warped) XW\n")
|
||||
})
|
||||
.collect();
|
||||
native_with_text(&(style.to_string() + &layer("Art", true, &art)), &doc)
|
||||
}
|
||||
|
||||
/// The type of `doc`, and whether it has a warp.
|
||||
fn warped_texts(doc: &Document) -> Vec<(String, bool)> {
|
||||
let mut out = vec![];
|
||||
doc.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
out.push((
|
||||
t.plain_text(),
|
||||
n.appearance.effects.iter().chain(n.appearance.items.iter().flat_map(|i| i.effects())).any(|e| e.id.starts_with("warp.")),
|
||||
));
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warped_type_that_draws_as_its_look_stays_live_type() {
|
||||
let art = warped_file(&warped_style(40.0, WarpOn::Object), "TILT\r", 1, &plain_warped_look());
|
||||
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
|
||||
assert_eq!(warped_texts(&d), [("TILT".to_string(), true)], "{notes:?}");
|
||||
assert!(!notes.iter().any(|w| w.starts_with(crate::import::ai::WARPED_TYPE_DRAWN)), "{notes:?}");
|
||||
}
|
||||
|
||||
/// What the live Arc at 40% draws of the type `TILT` that [`warped_file`] places, as look operators.
|
||||
fn plain_warped_look() -> String {
|
||||
let doc = text_document(&[("TILT\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]);
|
||||
let plain = layer("Art", true, &text_object(0));
|
||||
let (d, notes) = crate::ai_alone(&private(&native_with_text(&plain, &doc))).unwrap();
|
||||
let mut text = None;
|
||||
d.walk(|n| {
|
||||
if matches!(n.kind, NodeKind::Text(_)) {
|
||||
text = Some(n.clone());
|
||||
}
|
||||
});
|
||||
let mut text = text.unwrap_or_else(|| panic!("no type: {notes:?}"));
|
||||
text.appearance = warped_appearance().into();
|
||||
drawn_as_operators(&outlines_of(&vectorcraft_effects::expand_outlined(None, &text, None).unwrap()))
|
||||
}
|
||||
|
||||
/// The appearance that [`warped_style`] at 40% on the object reads as.
|
||||
fn warped_appearance() -> vectorcraft_doc::Appearance {
|
||||
let art = warped_file(&warped_style(40.0, WarpOn::Object), "TILT\r", 1, "");
|
||||
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
|
||||
let mut out = None;
|
||||
d.walk(|n| {
|
||||
if matches!(n.kind, NodeKind::Text(_)) {
|
||||
out = Some((*n.appearance).clone());
|
||||
}
|
||||
});
|
||||
out.unwrap_or_else(|| panic!("no type: {notes:?}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warped_type_that_draws_apart_from_its_look_comes_in_as_the_look() {
|
||||
// GUARDS: a warp on type opened live with no check, so a live warp that draws another shape from
|
||||
// the file's moved and resized the type.
|
||||
let drawing = "0 1 1 0 1 0 0 Xa\n54 56 m\n100 56 L\n100 80 L\n54 80 L\nf";
|
||||
for on in [WarpOn::Object, WarpOn::Fill, WarpOn::Stroke] {
|
||||
let art = warped_file(&warped_style(40.0, on), "TILT\r", 2, drawing);
|
||||
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
|
||||
assert_eq!(warped_texts(&d), [], "{notes:?}");
|
||||
let looks = d.layers[0].children().unwrap();
|
||||
assert_eq!(looks.len(), 2, "{looks:#?}");
|
||||
assert!(looks.iter().all(|l| matches!(l.kind, NodeKind::Group { .. }) && l.visual_bounds().is_some()), "{looks:#?}");
|
||||
// One note, with how many, that counts as a loss.
|
||||
let noted: Vec<&String> = notes.iter().filter(|w| w.starts_with(crate::import::ai::WARPED_TYPE_DRAWN)).collect();
|
||||
assert_eq!(noted, ["warped text objects that came in as drawn art, not as editable type: 2"], "{notes:?}");
|
||||
assert!(crate::is_loss(noted[0]));
|
||||
}
|
||||
}
|
||||
|
||||
/// Warped type that a text variable binds stays type with the effect live and keeps its binding,
|
||||
/// though its live warp draws apart from its look.
|
||||
#[test]
|
||||
fn warped_type_bound_to_a_text_variable_stays_bound_type() {
|
||||
use vectorcraft_testkit::ai::{XmlValue, named, variable_library_data, xml_element, xml_node};
|
||||
let attributes =
|
||||
[("trait", XmlValue::Str("textcontent")), ("varName", XmlValue::Unicode("v_text")), ("docRef", XmlValue::Raw("/XMLUIDREF : (type_x5F_1) ;"))];
|
||||
let declared = [xml_node(1, "variable", XmlValue::Str(""), &attributes, &[])];
|
||||
let set = xml_element("variableSet", &[("locked", "none")], &[xml_element("variables", &[], &declared)]);
|
||||
let library = variable_library_data(&xml_element("variableSets", &[], &[set]));
|
||||
let drawing = "0 1 1 0 1 0 0 Xa\n54 56 m\n100 56 L\n100 80 L\n54 80 L\nf";
|
||||
let own: String = text_object(0).lines().map(|l| format!("%_{l}\n")).collect();
|
||||
let art = format!("u\n{drawing}\nU\n9 () XW\n{own}{}1 (Warped) XW\n", named("type_x5F_1"));
|
||||
let doc = text_document(&[("TILT\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]);
|
||||
let file = native_with_text(&(library + &warped_style(40.0, WarpOn::Object) + &layer("Art", true, &art)), &doc);
|
||||
let (d, notes) = crate::ai_alone(&private(&file)).unwrap();
|
||||
assert_eq!(warped_texts(&d), [("TILT".to_string(), true)], "{notes:?}");
|
||||
assert!(!notes.iter().any(|w| w.starts_with(crate::import::ai::WARPED_TYPE_DRAWN)), "{notes:?}");
|
||||
let mut text = None;
|
||||
d.walk(|n| {
|
||||
if matches!(n.kind, NodeKind::Text(_)) {
|
||||
text = Some(n.id);
|
||||
}
|
||||
});
|
||||
assert_eq!(d.variables.objects_of("v_text"), [text.unwrap()], "{notes:?}");
|
||||
}
|
||||
|
||||
/// `operators` as the page's content: a red fill of the same paths.
|
||||
fn page_of(operators: &str) -> String {
|
||||
let mut page = String::from("1 0 0 setrgbcolor\n");
|
||||
for line in operators.lines() {
|
||||
let (args, op) = line.rsplit_once(' ').unwrap_or(("", line));
|
||||
let op = match op {
|
||||
"m" => "moveto",
|
||||
"L" => "lineto",
|
||||
"C" => "curveto",
|
||||
"f" => "closepath fill",
|
||||
_ => continue,
|
||||
};
|
||||
page += &format!("{args} {op}\n");
|
||||
}
|
||||
page
|
||||
}
|
||||
|
||||
/// Warped type that opens as its look is compared with the page again, as other art is.
|
||||
#[test]
|
||||
fn warped_type_that_comes_in_as_its_look_is_held_to_the_page() {
|
||||
let drawing = "0 1 1 0 1 0 0 Xa\n54 56 m\n100 56 L\n100 80 L\n54 80 L\nf";
|
||||
let art = warped_file(&warped_style(40.0, WarpOn::Object), "TILT\r", 1, drawing);
|
||||
let same = import(&eps(&page_of(drawing), &art)).unwrap();
|
||||
assert_eq!(warped_texts(&same.document), [], "{:?}", same.warnings);
|
||||
assert!(same.warnings.iter().any(|w| w.starts_with(crate::import::ai::WARPED_TYPE_DRAWN)), "{:?}", same.warnings);
|
||||
assert!(!same.warnings.iter().any(|w| w.contains("drew differently")), "{:?}", same.warnings);
|
||||
// A page that paints only part of the look's box: the box is compared, not left out.
|
||||
let other = page_of("60 60 m\n90 60 L\n90 75 L\n60 75 L\nf");
|
||||
let differs = import(&eps(&other, &art)).map(|r| r.warnings).unwrap_or_else(|why| vec![why.to_string()]);
|
||||
assert!(differs.iter().any(|w| w.contains("differs from the page") || w.contains("drew differently")), "{differs:?}");
|
||||
}
|
||||
|
||||
/// Warped type spends the page's share of the budget one object after another: with enough of them
|
||||
/// the later ones open as their looks, though each draws close enough on its own.
|
||||
#[test]
|
||||
fn warped_type_shares_the_page_budget() {
|
||||
let look = plain_warped_look();
|
||||
let style = warped_style(40.0, WarpOn::Object);
|
||||
let art = warped_file(&style, "TILT\r", 12, &look);
|
||||
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
|
||||
assert_eq!(warped_texts(&d).len(), 12, "{notes:?}");
|
||||
// The look draws a small square more than the live warp does.
|
||||
let art = warped_file(&style, "TILT\r", 12, &format!("{look}0 1 1 0 1 0 0 Xa\n2 2 m\n3.5 2 L\n3.5 3.5 L\n2 3.5 L\nf"));
|
||||
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
|
||||
let live = warped_texts(&d).len();
|
||||
assert!(live > 0 && live < 12, "{live}: {notes:?}");
|
||||
assert!(notes.iter().any(|w| w == &format!("{}: {}", crate::import::ai::WARPED_TYPE_DRAWN, 12 - live)), "{live}: {notes:?}");
|
||||
}
|
||||
|
||||
/// Envelopes and warped type share one count of checks: past it, warped type that would pass opens
|
||||
/// as its look.
|
||||
#[test]
|
||||
fn warped_type_past_the_envelopes_checks_comes_in_as_its_look() {
|
||||
let hidden = |s: &str| -> String { s.lines().map(|l| format!("%_{l}\n")).collect() };
|
||||
let rect = "20 5 m\n20 95 L\n80 95 L\n80 5 L\n20 5 L\n";
|
||||
let corner = |x: f64, y: f64, nx: f64, ny: f64, px: f64, py: f64| format!("[0.8 1 {x} {y} {nx} {ny} 1 {px} {py} 1] /N X#\n");
|
||||
let mesh = format!(
|
||||
"/Mesh X!\n2 /Version X#\n/Cartesian /Type X#\n[1 1] /Size X#\n/Data X#\n/DeviceGray /CS X#\n[0 0] /P X#\n{}{}{}{}/E X#\n/End X!\n",
|
||||
corner(20.0, 5.0, 20.0, 35.0, 40.0, 5.0),
|
||||
corner(20.0, 95.0, 40.0, 95.0, 20.0, 65.0),
|
||||
corner(80.0, 95.0, 80.0, 65.0, 60.0, 95.0),
|
||||
corner(80.0, 5.0, 60.0, 5.0, 80.0, 35.0),
|
||||
);
|
||||
let red = "0 1 1 0 1 0 0 Xa\n";
|
||||
let envelope = format!(
|
||||
"{}%_/ArtDictionary :\n%_131073 /Int (AIDeformOptionsKey) ,\n%_50 /Int (AIDeformFidelityKey) ,\n%_0 /Bool (AIDeformEditModeKey) ,\n%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_2 /Int (AIDeformStyle) ,\n%_;\n%_\nu\n{red}{rect}f\nU\n(Adobe Deform Plugin) 1 0 0 XP\n",
|
||||
hidden(&format!("u\n{mesh}U\n")),
|
||||
hidden(&format!("u\n{red}{rect}f\nU\n")),
|
||||
);
|
||||
let look = format!("u\n{}\nU\n9 () XW\n", plain_warped_look());
|
||||
let own: String = text_object(0).lines().map(|l| format!("%_{l}\n")).collect();
|
||||
let doc = text_document(&[("TILT\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]);
|
||||
let file = |envelopes: usize| {
|
||||
let body = format!("{}{look}{own}1 (Warped) XW\n", envelope.repeat(envelopes));
|
||||
native_with_text(&(warped_style(40.0, WarpOn::Object) + &layer("Art", true, &body)), &doc)
|
||||
};
|
||||
let (d, notes) = crate::ai_alone(&private(&file(1))).unwrap();
|
||||
assert_eq!(warped_texts(&d).len(), 1, "{notes:?}");
|
||||
let (d, notes) = crate::ai_alone(&private(&file(crate::import::ai::MAX_CHECKED))).unwrap();
|
||||
assert_eq!(warped_texts(&d), [], "{notes:?}");
|
||||
assert!(notes.iter().any(|w| w == &format!("{}: 1", crate::import::ai::WARPED_TYPE_DRAWN)), "{notes:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warped_type_with_a_look_that_draws_nothing_stays_type() {
|
||||
// The look only held the type, which the slot's text object replaces, or a path that paints
|
||||
// nothing: there is no art to stand in.
|
||||
for drawing in ["", LOOK_TYPE, "54 56 m\n100 56 L\n100 80 L\n54 80 L\nn"] {
|
||||
let art = warped_file(&warped_style(40.0, WarpOn::Object), "TILT\r", 1, drawing);
|
||||
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
|
||||
assert_eq!(warped_texts(&d), [("TILT".to_string(), true)], "{notes:?}");
|
||||
assert!(!notes.iter().any(|w| w.starts_with(crate::import::ai::WARPED_TYPE_DRAWN)), "{notes:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A look that draws type as well as art is missing the type once the slot's text object takes it,
|
||||
/// so it can't stand in: the object stays type, and its letters stay on the page.
|
||||
#[test]
|
||||
fn warped_type_whose_look_also_draws_type_stays_type() {
|
||||
let drawing = format!("{LOOK_TYPE}0 1 1 0 1 0 0 Xa\n54 56 m\n100 56 L\n100 80 L\n54 80 L\nf");
|
||||
let art = warped_file(&warped_style(40.0, WarpOn::Fill), "TILT\r", 1, &drawing);
|
||||
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
|
||||
assert_eq!(warped_texts(&d), [("TILT".to_string(), true)], "{notes:?}");
|
||||
assert!(!notes.iter().any(|w| w.starts_with(crate::import::ai::WARPED_TYPE_DRAWN)), "{notes:?}");
|
||||
}
|
||||
|
||||
/// A look with an object that wasn't read makes the file open from its page: the look isn't used.
|
||||
#[test]
|
||||
fn warped_type_whose_look_holds_an_unread_object_opens_from_the_page() {
|
||||
let drawing = "0 1 1 0 1 0 0 Xa\n54 56 m\n100 56 L\n100 80 L\n54 80 L\nf\nfrobnicate";
|
||||
let art = warped_file(&warped_style(40.0, WarpOn::Object), "TILT\r", 1, drawing);
|
||||
let page = page_of("54 56 m\n100 56 L\n100 80 L\n54 80 L\nf");
|
||||
let r = import(&eps(&page, &art)).unwrap();
|
||||
assert_eq!(warped_texts(&r.document), [], "{:?}", r.warnings);
|
||||
assert!(!r.warnings.iter().any(|w| w.starts_with(crate::import::ai::WARPED_TYPE_DRAWN)), "{:?}", r.warnings);
|
||||
assert!(r.warnings.iter().any(|w| w.contains("weren't read")), "{:?}", r.warnings);
|
||||
}
|
||||
|
||||
/// Warped frames of threaded type stay type, and stay one thread, whatever their looks draw.
|
||||
#[test]
|
||||
fn warped_threaded_type_stays_threaded() {
|
||||
let frames = [
|
||||
format!("<< /0 << /0 [ 0 0 ] /1 << /0 [ {} ] >> /2 << /0 1 >> >> >>", canvas_rect(10.0, 60.0, 40.0, 80.0)),
|
||||
format!("<< /0 << /0 [ 0 0 ] /1 << /0 [ {} ] >> /2 << /0 1 >> >> >>", canvas_rect(50.0, 60.0, 80.0, 80.0)),
|
||||
];
|
||||
let doc = document_of(&frames, &[story("A story in two frames\\r", &[0, 1], "/0 0", "/1 10.0")]);
|
||||
let body: String = (0..2)
|
||||
.map(|frame| {
|
||||
let own: String = frame_object(0, frame).lines().map(|l| format!("%_{l}\n")).collect();
|
||||
format!("u\n0 1 1 0 1 0 0 Xa\n2 2 m\n9 2 L\n9 9 L\n2 9 L\nf\nU\n9 () XW\n{own}1 (Warped) XW\n")
|
||||
})
|
||||
.collect();
|
||||
let art = native_with_text(&(warped_style(40.0, WarpOn::Object) + &layer("Art", true, &body)), &doc);
|
||||
let (d, notes) = crate::ai_alone(&private(&art)).unwrap();
|
||||
assert_eq!(d.text_threads.len(), 1, "{notes:?}");
|
||||
assert_eq!(warped_texts(&d).len(), 2, "{notes:?}");
|
||||
assert!(!notes.iter().any(|w| w.starts_with(crate::import::ai::WARPED_TYPE_DRAWN)), "{notes:?}");
|
||||
}
|
||||
|
||||
/// An art style section with style `Add`: Effect › Pathfinder › Add in the object's chain, over a
|
||||
/// fill and stroke that paint nothing.
|
||||
fn pathfinder_add_style() -> String {
|
||||
|
||||
@@ -6114,6 +6114,7 @@ breakText Glyphs Глифи
|
||||
Gap 5 Разстояние 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: те са внесени като изображението от страницата
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} изкривени текстови обекти, внесени като изображение, а не като редактируем текст: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} обектите, изобразени различно от собствената страница на файла, са внесени като изображението от страницата: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Трябва да изберете страничен шаблон.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6148,6 +6148,7 @@ shape Open Otvoren
|
||||
@msg the paths have nothing to merge there putanje tu nemaju šta spojiti
|
||||
@msg the path is locked or hidden putanja je zaključana ili skrivena
|
||||
@msg {_1}: they came in as the page's art {_1}: uvezeni su kao ilustracija stranice
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} iskrivljeni tekstualni objekti uvezeni kao ilustracija, a ne kao tekst koji se može uređivati: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} objekti koji su se iscrtali drugačije od vlastite stranice datoteke uvezeni su kao ilustracija stranice: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Morate odabrati bočni uzorak.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6248,6 +6248,7 @@ breakText Glyphs Glifs
|
||||
Gap 5 Separació 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: han entrat com a art de la pàgina
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} objectes de text deformats que han entrat com a art, no com a text editable: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} els objectes que es dibuixaven diferent de la pàgina pròpia del fitxer han entrat com a art de la pàgina: {_1}
|
||||
|
||||
@msg the JPEG 2000 file can't be read no es pot llegir el fitxer JPEG 2000
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6264,6 +6264,7 @@ breakText Glyphs Glyfy
|
||||
Gap 5 Mezera 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: byly načteny jako kresba ze stránky
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} pokřivené textové objekty načtené jako kresba, ne jako upravitelný text: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} objekty, které se vykreslily jinak než vlastní stránka souboru, byly načteny jako kresba ze stránky: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Musíte zvolit boční vzorek.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6128,6 +6128,7 @@ breakText Glyphs Glyffer
|
||||
Gap 5 Mellemrum 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: de blev importeret som sidens grafik
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} vredne tekstobjekter, der blev importeret som grafik og ikke som redigerbar tekst: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} objekter, der blev tegnet anderledes end filens egen side, blev importeret som sidens grafik: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Du skal vælge et sidemønster.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5850,6 +5850,7 @@ breakText Glyphs Glyphen
|
||||
Gap 5 Lücke 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: sie wurden als Seitengrafik importiert
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} verkrümmte Textobjekte, die als Grafik und nicht als bearbeitbarer Text importiert wurden: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} Objekte, die anders als die eigene Seite der Datei gezeichnet wurden, wurden als Seitengrafik importiert: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Sie müssen ein Seitenmuster auswählen.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6216,6 +6216,7 @@ breakText Glyphs Γλύφοι
|
||||
Gap 5 Διάκενο 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: εισήχθησαν ως γραφικά της σελίδας
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} παραμορφωμένα αντικείμενα κειμένου που εισήχθησαν ως γραφικά, όχι ως επεξεργάσιμο κείμενο: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} τα αντικείμενα που σχεδιάστηκαν διαφορετικά από την ίδια τη σελίδα του αρχείου εισήχθησαν ως γραφικά της σελίδας: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Πρέπει να επιλέξετε πλευρικό μοτίβο.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 163 and column 5.
|
@@ -6206,6 +6206,7 @@ breakText Glyphs Glifos
|
||||
Gap 5 Hueco 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: se importaron como el arte de la página
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} objetos de texto deformados que se importaron como arte, no como texto editable: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} los objetos que se dibujaban de forma distinta a la página del propio archivo se importaron como el arte de la página: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Debe elegir un motivo lateral.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6294,6 +6294,7 @@ breakText Glyphs Merkit
|
||||
Gap 5 Väli 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: ne tuotiin sivun piirroksena
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} vääristetyt tekstiobjektit, jotka tuotiin piirroksena eikä muokattavana tekstinä: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} objektit, jotka piirtyivät eri tavoin kuin tiedoston oma sivu, tuotiin sivun piirroksena: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Valitse sivukuvio.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6259,6 +6259,7 @@ breakText Glyphs Glyphes
|
||||
Gap 5 Écart 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1} : ils ont été importés comme le dessin de la page
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} objets texte déformés importés comme dessin et non comme texte modifiable : {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} les objets dessinés différemment de la page du fichier lui-même ont été importés comme le dessin de la page : {_1}
|
||||
|
||||
@msg You must choose a side pattern. Vous devez choisir un motif latéral.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6874,6 +6874,7 @@
|
||||
@msg objects can't be nested more than 32 deep ऑब्जेक्ट्स को 32 से अधिक गहराई तक घोंसला नहीं किया जा सकता है
|
||||
@msg objects inside a blend, envelope, repeat or Intertwine group can't be attached to a path एक मिश्रण, लिफ़ाफ़ाफ़ा, दोहराने या अंतर्जातीय समूह के अंदर ऑब्जेक्ट्स को एक पाथ से जोड़ा नहीं जा सकता है
|
||||
@msg objects must be none, move or copy objects: ऑब्जेक्टएं कोई नहीं, चाल या प्रतिलिपि होना चाहिए
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} मोड़े गए टेक्स्ट ऑब्जेक्ट जो संपादन योग्य टेक्स्ट के बजाय कला के रूप में आए: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} उन ऑब्जेक्ट्स को जो फ़ाइल के स्वयं के पृष्ठ से अलग-अलग तरीके से आकर्षित होते हैं, उनकी कला के रूप में आया: {_1}
|
||||
@msg objects were left out ऑब्जेक्ट्स को छोड़ दिया गया था
|
||||
@msg objects with several fills or strokes, effects or brushes came in as their drawn look ({_1}): their appearance isn't editable yet कई भराव या स्ट्रोक, इफ़ेक्ट या ब्रश के साथ ऑब्जेक्टएं उनके ड्रॉइंग लुक ({_1}) के रूप में आईं: उनकी उपस्थिति अभी तक संपादन योग्य नहीं है
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -6248,6 +6248,7 @@ breakText Glyphs Glifák
|
||||
Gap 5 Hézag 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: az oldal rajzaként lettek importálva
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} hajlított szövegobjektumok, amelyek rajzként, nem szerkeszthető szövegként lettek importálva: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} a fájl saját oldalától eltérően rajzolt objektumok az oldal rajzaként lettek importálva: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Válasszon oldalsó mintát.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6128,6 +6128,7 @@ breakText Glyphs Glif
|
||||
Gap 5 Jarak 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: diimpor sebagai gambar halaman
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} objek teks dengan Warp yang diimpor sebagai gambar, bukan sebagai teks yang dapat diedit: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} objek yang tergambar berbeda dari halaman file itu sendiri diimpor sebagai gambar halaman: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Anda harus memilih pola sisi.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2246 and column 5.
|
@@ -5884,6 +5884,7 @@ breakText Glyphs Glifi
|
||||
Gap 5 Spazio 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: sono stati importati come elementi grafici della pagina
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} oggetti di testo alterati importati come elementi grafici, non come testo modificabile: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} gli oggetti disegnati in modo diverso dalla pagina del file stesso sono stati importati come elementi grafici della pagina: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Devi scegliere un motivo laterale.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5949,6 +5949,7 @@ breakText Glyphs 字形
|
||||
Gap 5 間隔 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: ページのアートとして読み込みました
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} 編集可能なテキストではなくアートとして読み込んだワープのテキストオブジェクト: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} ファイル自身のページと描画が異なるオブジェクトをページのアートとして読み込みました: {_1}
|
||||
|
||||
@msg You must choose a side pattern. 側面パターンを選択してください。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6261,6 +6261,7 @@ breakText Glyphs გლიფები
|
||||
Gap 5 შუალედი 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: იმპორტირებულია გვერდის გამოსახულების სახით
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} გაღუნული ტექსტური ობიექტები, რომლებიც იმპორტირებულია გამოსახულების და არა რედაქტირებადი ტექსტის სახით: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} ობიექტები, რომლებიც ფაილის საკუთარი გვერდისგან განსხვავებულად იხატებოდა, იმპორტირებულია გვერდის გამოსახულების სახით: {_1}
|
||||
|
||||
@msg You must choose a side pattern. აირჩიეთ გვერდითი ნიმუში.
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5845,6 +5845,7 @@ breakText Glyphs 글리프
|
||||
Gap 5 간격 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: 페이지의 아트로 가져왔습니다
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} 편집 가능한 문자가 아닌 아트로 가져온 왜곡된 문자 오브젝트: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} 파일 자체 페이지와 다르게 그려진 오브젝트를 페이지의 아트로 가져왔습니다: {_1}
|
||||
|
||||
@msg You must choose a side pattern. 측면 패턴을 선택해야 합니다.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6174,6 +6174,7 @@ breakText Glyphs Glyphs
|
||||
Gap 5 Tussenruimte 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: ze zijn geïmporteerd als pagina-afbeelding
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} tekstobjecten met Warp die zijn geïmporteerd als afbeelding, niet als bewerkbare tekst: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} objecten die anders werden getekend dan de eigen pagina van het bestand, zijn geïmporteerd als pagina-afbeelding: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Kies een zijpatroon.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6119,6 +6119,7 @@ breakText Glyphs Glify
|
||||
Gap 5 Odstęp 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: zaimportowano je jako grafikę strony
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} wypaczone obiekty tekstowe zaimportowane jako grafika, a nie jako edytowalny tekst: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} obiekty rysowane inaczej niż własna strona pliku zaimportowano jako grafikę strony: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Musisz wybrać wzór boczny.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6182,6 +6182,7 @@ breakText Glyphs Glifos
|
||||
Gap 5 Lacuna 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: foram importados como arte da página
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} objetos de texto distorcidos importados como arte, não como texto editável: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} os objetos desenhados de forma diferente da própria página do arquivo foram importados como arte da página: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Você deve escolher um padrão lateral.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -6241,6 +6241,7 @@ breakText Glyphs Глифы
|
||||
Gap 5 Зазор 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: импортированы как графика страницы
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} деформированные текстовые объекты, импортированные как графика, а не как редактируемый текст: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} объекты, которые отрисовывались иначе, чем собственная страница файла, импортированы как графика страницы: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Выберите боковой узор.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -6316,6 +6316,7 @@ breakText Glyphs Glyfer
|
||||
Gap 5 Mellanrum 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: de importerades som sidans bild
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} förvrängda textobjekt som importerades som bild och inte som redigerbar text: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} objekt som ritades annorlunda än filens egen sida importerades som sidans bild: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Du måste välja ett sidomönster.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1606,6 +1606,19 @@ fn messages_with_a_count_take_the_plural_form() {
|
||||
assert_eq!(message(cs, "Packaged 21 files"), "Zabaleno 21 souborů");
|
||||
}
|
||||
|
||||
/// The import's note for warped type that opened as drawn art is translated in every complete
|
||||
/// catalog. It is built from a constant, which the scan of message literals doesn't see.
|
||||
#[test]
|
||||
fn warped_type_note_is_translated() {
|
||||
let note = "warped text objects that came in as drawn art, not as editable type: 3";
|
||||
for code in COMPLETE_MESSAGES {
|
||||
let lang = Lang::from_code(code).expect("registered");
|
||||
let shown = message(lang, note);
|
||||
assert_ne!(shown, note, "{code}");
|
||||
assert!(shown.contains('3'), "{code}: {shown}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The Gamut Check status line names a count of 1 in the singular and other counts in the plural,
|
||||
/// and every complete catalog translates it, in the form its plural rule picks.
|
||||
#[test]
|
||||
|
||||
@@ -6221,6 +6221,7 @@ breakText Glyphs Glifler
|
||||
Gap 5 Boşluk 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: sayfanın çizimi olarak içe aktarıldılar
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} düzenlenebilir metin yerine çizim olarak içe aktarılan Çözgü uygulanmış metin nesneleri: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} dosyanın kendi sayfasından farklı çizilen nesneler sayfanın çizimi olarak içe aktarıldı: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Bir yan desen seçmelisiniz.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -5864,6 +5864,7 @@ breakText Glyphs Гліфи
|
||||
Gap 5 Проміжок 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: їх імпортовано як графіку сторінки
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} деформовані текстові об’єкти, імпортовані як графіка, а не як редагований текст: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} об’єкти, що малювалися інакше, ніж власна сторінка файлу, імпортовано як графіку сторінки: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Виберіть бічний візерунок.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 177 and column 5.
|
@@ -5865,6 +5865,7 @@ breakText Glyphs Glyph
|
||||
Gap 5 Khoảng hở 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}: đã được nhập dưới dạng hình vẽ của trang
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} các đối tượng văn bản biến dạng được nhập dưới dạng hình vẽ, không phải văn bản có thể chỉnh sửa: {_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} các đối tượng được vẽ khác với trang riêng của tệp đã được nhập dưới dạng hình vẽ của trang: {_1}
|
||||
|
||||
@msg You must choose a side pattern. Bạn phải chọn mẫu bên.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 199 and column 5.
|
@@ -6105,6 +6105,7 @@ breakText Glyphs 字形
|
||||
Gap 5 间隔 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}:已作为页面图稿导入
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} 作为图稿而非可编辑文字导入的变形文字对象:{_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} 与文件自身页面绘制不同的对象已作为页面图稿导入:{_1}
|
||||
|
||||
@msg You must choose a side pattern. 必须选择侧边图案。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5902,6 +5902,7 @@ breakText Glyphs 字符
|
||||
Gap 5 間隙 5
|
||||
|
||||
@msg {_1}: they came in as the page's art {_1}:已作為頁面圖稿匯入
|
||||
@msg warped text objects that came in as drawn art, not as editable type: {_1} 作為圖稿而非可編輯文字匯入的彎曲文字物件:{_1}
|
||||
@msg objects that drew differently from the file's own page came in as its art: {_1} 與檔案本身頁面繪製不同的物件已作為頁面圖稿匯入:{_1}
|
||||
|
||||
@msg You must choose a side pattern. 必須選擇側邊圖樣。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -134,6 +134,11 @@ private editing data. VectorCraft:
|
||||
brushes come in as their drawn look (#637; pattern fills are read since #1025, and symbols and their instances
|
||||
open live); legacy `.ai` without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868; guides
|
||||
open as ruler guides and guide paths since #5057 (#779).
|
||||
Type under a Warp effect is compared with the art saved with it: type that draws as that art stays editable with
|
||||
the effect live, and type that draws differently, or that the file's limit of checks has no check left for, opens
|
||||
as that art, with one note that counts them. Type stays live without a comparison when the saved art paints
|
||||
nothing or draws type of its own, in frames of threaded type, when a text variable binds it, and when its text
|
||||
object doesn't come in as one text object.
|
||||
Legacy `.ai` type keeps its paragraphs' letter and word spacing (`TC`, `TW`), hanging punctuation (`Tq`) and
|
||||
hyphenation (`Th`); its tab definitions (`Xb` … `XB`) set its paragraphs' tab stops. Paragraphs in a row with
|
||||
the same attributes share one copy of them while the text object is read, and those copies hold at most
|
||||
|
||||
@@ -1273,6 +1273,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Blends and envelopes in the definitions of a `.ai`'s symbols, patterns and brushes' art count against the same per-file limits on blend layout and envelope checks as the layers' art, so a file with many definitions does not get those limits again for each one (`crates/eps/src/import/ai/read/directives.rs`).
|
||||
A `.ai` or EPS whose editing data has brushes opens with the file's Calligraphic, Scatter, Art and Bristle brushes in the Brushes panel in place of the default ones, in the file's order: the Calligraphic size, roundness and angle with their variation, the Scatter size, spacing, scatter and rotation, the Bristle settings, and the Scatter and Art brushes' art. A brush library `.ai` (brushes and no layers) opens from its PDF part with its brushes, so Other Library lists them. An Art brush in Tints keeps that colorization. Pattern brushes and brushes that can't be read are left out with a note. Paths with a Calligraphic brush, and paths with an Art brush at 100% in Tints that stretches art of solid-filled paths unflipped, open as the path with the brush on its stroke, and other brushed paths still come in as their drawn look (part of #372 and #637; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
In a `.ai` opened from its editing data, a path or type object with Offset Path, Outline Object or Outline Stroke on the whole object opens as itself with those effects live, in their order and shown or hidden; on a fill or stroke of type they stay on that fill or stroke. Offset Path keeps its Offset, Joins and Miter Limit, and Outline Stroke takes its width from the stroke. An Offset Path with a setting missing, out of range or not read keeps the drawn art, as does a path with a Path effect on one fill or stroke (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
In a `.ai` opened from its editing data, type with Warp on the object or on one of its fills or strokes opens as type with the effect live when the live warp draws as the art saved with it. When it draws differently, or when the file's limit of checks has no check left for it, it opens as that art, and one note says how many did. Type stays live without a comparison when the saved art paints nothing or draws type of its own, in frames of threaded type, when a text variable binds it, and when its text object doesn't come in as one text object (`crates/eps/src/import/ai/envelope.rs`, `crates/eps/src/import/tests_native.rs`).
|
||||
In a `.ai` opened from its editing data, an object with a Tweak that moves nothing or Pucker & Bloat at 0 opens as itself with the effect live. A path, compound path or group whose only effect is Transform without copies, or a Free Distort whose corners make a parallelogram, opens as itself with it live, and so does an unstroked path or compound path with straight segments whose only effect is Twist. A gradient or pattern, an image, type, a clipping group, an opacity mask or effects inside the object, or a stroke with a Transform that scales, rotates or reflects, keep the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/ai/read/ops.rs`).
|
||||
A PDF page whose `UserUnit` (PDF 1.6) is above 1 opens at its size in points, its numbers times the `UserUnit` (one above 75,000 counts as 75,000), and `document.pdfInfo` gives its size and boxes in points too. A `.ai` whose editing data has its artboards in the units of such a page opens from that data at the page's size, with its layers, and when the layers can't be read its pages still take the artboard layout of the data, scaled the same way (part of #1482).
|
||||
When the PDF content of a `.ai` has a page for each artboard, the layers are compared with every page on its artboard (until the pages compared take 8,388,608 pixels at the comparison's scale, about 67 letter-size pages, or number 128): art that only the layers or only a later page draws is handled as art that differs on the first page is, and a text object that a later page doesn't draw is left out unless its layer doesn't print (`crates/eps/src/import/tests_native.rs`).
|
||||
|
||||
Reference in New Issue
Block a user