mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 09:29:44 -06:00
Fixes #2763. An object whose editing-data dictionary holds the text wrap options keeps them (offset in points, 6 pt by default, clamped to ±1000 pt; Invert Wrap), and the objects after the text in a version 7 wraparound group (`*w` … `*W`) are wrap objects at the default options. Area type below a wrap object in its layer flows around it before the layers are compared with the page, and threaded type flows on into its next frames. A Large Canvas file's offset is scaled with its art. The wrap resolver moves into `vectorcraft_doc::text::resolve_wrap` for the import and the edit-time refresh, and builds each wrap object's outline once per layer. A file whose wrap shapes would copy more than 2,000,000 anchors is not read from its editing data. Docs: docs/file-format-parity.md.
614 lines
28 KiB
Rust
614 lines
28 KiB
Rust
use super::art::{element_name, elements, join_and, sane};
|
|
use super::containers::drop_commented_copies;
|
|
use super::types::{FLIP, Kind, MAX_DEPTH};
|
|
use super::{
|
|
Affine, Arc, Artboard, CANVAS_CENTRE, ColorMode, Document, Node, NodeId, NodeKind, Obj, OpacityMask, Paint, Point, Reader, Rect, Structure,
|
|
Swatch, Unit, V, obj,
|
|
};
|
|
use vectorcraft_doc::{Guide, ImageBlob, PatternDef, TextWrap};
|
|
|
|
/// `%AI5_RulerUnits` → [`Unit`] (Illustrator 7 spec table 3; centimeters is 4; pixels is 6 in later files).
|
|
fn ruler_unit(v: u8) -> Option<Unit> {
|
|
Some(match v {
|
|
0 => Unit::Inches,
|
|
1 => Unit::Millimeters,
|
|
2 => Unit::Points,
|
|
3 => Unit::Picas,
|
|
4 => Unit::Centimeters,
|
|
6 => Unit::Pixels,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
/// The name of the empty group that stands for a text object, followed by the number of its
|
|
/// story in the text document when the file says.
|
|
pub(crate) const TEXT_SLOT: &str = "\u{0}text";
|
|
|
|
/// Is `name` that of a text slot, and which story is it of?
|
|
pub(crate) fn slot_of(name: Option<&str>) -> Option<Option<u32>> {
|
|
name?.strip_prefix(TEXT_SLOT).map(|rest| rest.split(':').next().and_then(|s| s.parse().ok()))
|
|
}
|
|
|
|
/// Which of its story's frames a text slot named `name` is (0 when it doesn't say).
|
|
pub(crate) fn slot_frame(name: Option<&str>) -> usize {
|
|
name.and_then(|n| n.strip_prefix(TEXT_SLOT)).and_then(|rest| rest.split_once(':')).and_then(|(_, f)| f.parse().ok()).unwrap_or(0)
|
|
}
|
|
|
|
/// The artboards (names, and rects in art space) of the editing data `data` that [`read`] can't
|
|
/// read as a whole, as far as it gets: where to put the pages of its PDF part instead, and what to
|
|
/// call them (#1068).
|
|
pub fn artboards(data: &[u8]) -> Vec<(String, Rect)> {
|
|
let found = artboards_read(data);
|
|
if !found.is_empty() {
|
|
return found;
|
|
}
|
|
// Reading can stop before the artboards (art kept in the document's data comes first): read
|
|
// their array on its own.
|
|
artboard_array(data).map_or(found, |array| {
|
|
let mut alone = b"%_/Document :\n%_/Dictionary :\n".to_vec();
|
|
alone.extend_from_slice(array);
|
|
alone.extend_from_slice(b"\n%_; /NotRecorded ,\n%_;\n");
|
|
artboards_read(&alone)
|
|
})
|
|
}
|
|
|
|
/// The `ArtboardArray` entry of the editing data `data`, from its `/Array :` to its key: found
|
|
/// from the key back, counting the dictionaries its artboards open and close.
|
|
fn artboard_array(data: &[u8]) -> Option<&[u8]> {
|
|
const KEY: &[u8] = b"%_; (ArtboardArray) ,";
|
|
let end = data.windows(KEY.len()).rposition(|w| w == KEY)? + KEY.len();
|
|
let head = data.get(..end)?;
|
|
let mut depth = 0usize;
|
|
let mut line_end = head.len();
|
|
loop {
|
|
let line_start = head.get(..line_end)?.iter().rposition(|&b| b == b'\n' || b == b'\r').map_or(0, |at| at + 1);
|
|
if let Some(text) = head.get(line_start..line_end)?.strip_prefix(b"%_") {
|
|
if text.starts_with(b";") || text.starts_with(b"/Def ;") {
|
|
depth += 1;
|
|
} else if text.starts_with(b"/") && text.windows(2).any(|w| w == b" :") {
|
|
depth = depth.checked_sub(1)?;
|
|
if depth == 0 {
|
|
return head.get(line_start..);
|
|
}
|
|
}
|
|
}
|
|
line_end = line_start.checked_sub(1)?;
|
|
}
|
|
}
|
|
|
|
/// The palette of a `.ai` file's editing data, as [`palette`] reads it.
|
|
pub struct PaletteSwatches {
|
|
pub swatches: Vec<Swatch>,
|
|
pub groups: Vec<vectorcraft_color::swatch::SwatchGroup>,
|
|
/// The patterns the editing data defines that the given patterns lack and a swatch paints with.
|
|
pub patterns: Vec<PatternDef>,
|
|
/// The image blobs the art of `patterns` shows, by key.
|
|
pub images: std::collections::BTreeMap<String, ImageBlob>,
|
|
}
|
|
|
|
/// The swatches and color groups of the palette of the editing data `data`, for a file whose
|
|
/// layers aren't read: the swatches its PDF part has are the defaults (#4036). They come as
|
|
/// [`read`] makes them, with pattern swatches only for the patterns in `patterns` and those the
|
|
/// editing data defines; `None` when the palette is empty or reading stops before it.
|
|
pub fn palette(data: &[u8], patterns: &[PatternDef]) -> Option<PaletteSwatches> {
|
|
let mut r = Reader::new(data);
|
|
// The palette, the gradients and the patterns it names come before the art, so an error in the
|
|
// art leaves them read.
|
|
let _ = r.run();
|
|
let mut known = patterns.to_vec();
|
|
let defined = std::mem::take(&mut r.doc.patterns);
|
|
known.extend(defined.iter().filter(|d| !patterns.iter().any(|p| p.name == d.name)).cloned());
|
|
let (swatches, groups) = r.palette_swatches(&known)?;
|
|
let painted = |name: &str| {
|
|
swatches.iter().chain(groups.iter().flat_map(|g| &g.swatches)).any(|s| matches!(&s.paint, Paint::Pattern { pattern, .. } if pattern == name))
|
|
};
|
|
let added: Vec<PatternDef> = defined.into_iter().filter(|d| !patterns.iter().any(|p| p.name == d.name) && painted(&d.name)).collect();
|
|
let mut shown = Document::new(1.0, 1.0);
|
|
shown.patterns.clone_from(&added);
|
|
let mut images = std::collections::BTreeMap::new();
|
|
shown.visit_images(|_, im| {
|
|
if let Some(blob) = r.doc.images.get(&im.key) {
|
|
images.insert(im.key.clone(), blob.clone());
|
|
}
|
|
});
|
|
Some(PaletteSwatches { swatches, groups, patterns: added, images })
|
|
}
|
|
|
|
/// [`artboards`] as far as reading `data` from its start gets: those read before an error, and
|
|
/// those of a `/Document` still open when it stopped (its art can be what stopped it).
|
|
fn artboards_read(data: &[u8]) -> Vec<(String, Rect)> {
|
|
let mut r = Reader::new(data);
|
|
let _ = r.run();
|
|
let open: Vec<Obj> = r.frames.iter().filter_map(|f| if let Kind::Obj(o) = &f.kind { Some((**o).clone()) } else { None }).collect();
|
|
for o in &open {
|
|
if r.artboards.is_empty() {
|
|
r.document(o);
|
|
}
|
|
}
|
|
r.artboards
|
|
}
|
|
|
|
/// How many variables and data sets the variable library of the editing data has: XML nodes
|
|
/// (`/XMLNode`) of the `/Document` dictionary, a `variableSets` element holding `variableSet`s, each
|
|
/// with its `variables` (`variable` elements) and `sampleDataSets` (`sampleDataSet` elements).
|
|
/// VectorCraft doesn't read them.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub struct VariableLibrary {
|
|
pub variables: usize,
|
|
pub data_sets: usize,
|
|
}
|
|
|
|
impl VariableLibrary {
|
|
/// The import's note that the library wasn't read, when it has a variable or a data set.
|
|
pub fn note(self) -> Option<String> {
|
|
let Self { variables, data_sets } = self;
|
|
// Counts after a colon need no plural, so every language can translate the note whole.
|
|
(variables + data_sets > 0).then(|| {
|
|
format!("the file's variables and data sets weren't read (variables: {variables}, data sets: {data_sets}): the Variables panel doesn't list them")
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The variable library of the editing data `data` ([`VariableLibrary`]), for a file whose layers
|
|
/// [`read`] can't read. Data without a `variableSets` element isn't read.
|
|
pub fn variables(data: &[u8]) -> VariableLibrary {
|
|
if crate::import::ai::find(data, b"variableSets").is_none() {
|
|
return VariableLibrary::default();
|
|
}
|
|
let mut r = Reader::new(data);
|
|
let _ = r.run();
|
|
// A `/Document` still open when reading stopped.
|
|
let open: Vec<Obj> = r.frames.iter().filter_map(|f| if let Kind::Obj(o) = &f.kind { Some((**o).clone()) } else { None }).collect();
|
|
for o in open.iter().filter(|o| o.ty == "Document") {
|
|
variable_library(o, 0, &mut r.variables);
|
|
}
|
|
if r.variables != VariableLibrary::default() {
|
|
return r.variables;
|
|
}
|
|
// Reading can stop before the library: read its node on its own.
|
|
variable_sets_node(data).map_or(r.variables, |node| {
|
|
let mut alone = b"%_/Document :\n%_/Dictionary :\n%_/Array :\n".to_vec();
|
|
alone.extend_from_slice(node);
|
|
alone.extend_from_slice(b"\n%_; (xmlnode-children) ,\n%_; /Recorded ,\n%_;\n");
|
|
let mut r = Reader::new(&alone);
|
|
let _ = r.run();
|
|
r.variables
|
|
})
|
|
}
|
|
|
|
/// The `variableSets` XML node of the editing data `data`, from its `/XMLNode :` line to the line
|
|
/// that closes it: found from its name back, counting the dictionaries and arrays it holds.
|
|
fn variable_sets_node(data: &[u8]) -> Option<&[u8]> {
|
|
const NAME: &[u8] = b"variableSets) /String (xmlnode-nodename) ,";
|
|
let at = data.windows(NAME.len()).position(|w| w == NAME)?;
|
|
let opens = |text: &[u8]| text.starts_with(b"/") && text.windows(2).any(|w| w == b" :");
|
|
let closes = |text: &[u8]| text.starts_with(b";") || text.starts_with(b"/Def ;");
|
|
let mut depth = 0usize;
|
|
let mut line_end = data.get(..at)?.iter().rposition(|&b| b == b'\n' || b == b'\r')?;
|
|
let start = loop {
|
|
let line_start = data.get(..line_end)?.iter().rposition(|&b| b == b'\n' || b == b'\r').map_or(0, |at| at + 1);
|
|
if let Some(text) = data.get(line_start..line_end)?.strip_prefix(b"%_") {
|
|
if closes(text) {
|
|
depth += 1;
|
|
} else if opens(text) {
|
|
match depth.checked_sub(1) {
|
|
Some(d) => depth = d,
|
|
None => break line_start,
|
|
}
|
|
}
|
|
}
|
|
line_end = line_start.checked_sub(1)?;
|
|
};
|
|
let mut line_start = start;
|
|
loop {
|
|
let rest = data.get(line_start..)?;
|
|
let line_end = line_start + rest.iter().position(|&b| b == b'\n' || b == b'\r').unwrap_or(rest.len());
|
|
if let Some(text) = data.get(line_start..line_end)?.strip_prefix(b"%_") {
|
|
if closes(text) {
|
|
depth = depth.checked_sub(1)?;
|
|
if depth == 0 {
|
|
return data.get(start..line_end);
|
|
}
|
|
} else if opens(text) {
|
|
depth += 1;
|
|
}
|
|
}
|
|
if line_end >= data.len() {
|
|
return None;
|
|
}
|
|
line_start = line_end + 1;
|
|
}
|
|
}
|
|
|
|
/// The text wrap options an object's dictionary gives: its offset in points (`AI11TextWrapOffset`,
|
|
/// times `scale`, the factor that takes the art onto the document) and Invert Wrap
|
|
/// (`AI11TextWrapInvert`). The outline the file may keep with them (`AI11TextWrapPath`) is not
|
|
/// read: the object's own shape gives it. A value that isn't a number is left at its default, and
|
|
/// a dictionary without a readable entry gives none.
|
|
fn text_wrap(o: &Obj, scale: f64) -> Option<TextWrap> {
|
|
let num = |key: &str| o.get(key).and_then(|v| v.iter().find_map(V::num)).filter(|v| v.is_finite());
|
|
let offset = num("AI11TextWrapOffset");
|
|
let invert = num("AI11TextWrapInvert").filter(|v| *v == 0.0 || *v == 1.0);
|
|
if offset.is_none() && invert.is_none() && o.get("AI11TextWrapPath").is_none() {
|
|
return None;
|
|
}
|
|
let default = TextWrap::default();
|
|
let offset = offset.map(|v| v * scale).filter(|v| !v.is_nan()).map_or(default.offset, |v| v.clamp(-1000.0, 1000.0));
|
|
Some(TextWrap { offset, invert: invert == Some(1.0) })
|
|
}
|
|
|
|
impl<'a> Reader<'a> {
|
|
/// An object's dictionary: its name (`AIArtName`, or the XML id `AI10_ArtUID`), and the
|
|
/// opacity mask it may carry.
|
|
pub(super) fn art_dictionary(&mut self, o: &Obj) {
|
|
self.constraint_art(o);
|
|
// A blend's key object or spine.
|
|
if let Some(kind) = o.text("ShapeBlendPathType")
|
|
&& let Some(id) = self.last_object().map(|n| n.id)
|
|
&& self.blend_roles.len() < 100_000
|
|
{
|
|
self.blend_roles.insert(id, (kind, o.text("ShapeBlendPathID")));
|
|
}
|
|
// An envelope (a distortion, `AIDeformStyle` / `DeformValue`) is written on `%_` lines as
|
|
// a group holding the mesh that shapes it (or its contents, while they are being edited);
|
|
// the distorted art the file's PDF part draws follows on plain lines. That group isn't
|
|
// art, so it goes (#2405), kept aside for the live envelope ([`Reader::end_envelope`]).
|
|
if (o.get("AIDeformStyle").is_some() || o.get("DeformValue").is_some()) && self.drop_envelope_art() {
|
|
return;
|
|
}
|
|
if let Some(mask) = o.obj("AI9_Mask").or_else(|| o.obj("Mask")) {
|
|
if let Some(art) = &mask.art {
|
|
let on = |key: &str| mask.nums(key).first().copied() == Some(1.0);
|
|
let value =
|
|
OpacityMask { art: art.clone(), clip: on("Clipping"), invert: on("Inverted"), disabled: on("Disabled"), linked: on("Linked") };
|
|
if let Some(n) = self.last_object().filter(|n| !matches!(n.kind, NodeKind::Layer { .. })) {
|
|
n.extras_mut().mask = Some(Box::new(value));
|
|
} else if !on("Disabled") {
|
|
self.unreadable("opacity masks without an owning object", false);
|
|
}
|
|
// What the art doesn't read matters only when the mask shapes what shows.
|
|
for what in &mask.unread {
|
|
if on("Disabled") {
|
|
self.hidden_unread.insert(what.clone());
|
|
} else if let Some(op) = what.strip_prefix('`').and_then(|w| w.strip_suffix('`')) {
|
|
self.note_unknown(op, false);
|
|
} else {
|
|
self.unreadable(what, false);
|
|
}
|
|
}
|
|
} else if mask.nums("Disabled").first().copied() != Some(1.0) {
|
|
self.unreadable("opacity masks without readable art", false);
|
|
}
|
|
}
|
|
// Text wrap: area type below the object in its layer flows around it.
|
|
if let Some(wrap) = text_wrap(o, self.large_canvas.unwrap_or(1.0))
|
|
&& let Some(n) = self.last_object().filter(|n| !matches!(n.kind, NodeKind::Layer { .. }))
|
|
{
|
|
n.extras_mut().wrap = Some(wrap);
|
|
}
|
|
let name = match o.text("AIArtName") {
|
|
Some(name) => name,
|
|
None => match o.obj("AI10_ArtUID").and_then(Obj::content_text) {
|
|
Some(id) => obj::xml_name(&id),
|
|
None => return,
|
|
},
|
|
};
|
|
let Some(n) = self.last_object().filter(|n| !matches!(n.kind, NodeKind::Layer { .. }) && !name.is_empty()) else { return };
|
|
if slot_of(n.name.as_deref()).is_some() {
|
|
let id = n.id;
|
|
self.slot_names.insert(id, name);
|
|
} else {
|
|
n.name = Some(name);
|
|
}
|
|
}
|
|
|
|
/// The `/Document` dictionary: the artboards, how many variables and data sets its variable
|
|
/// library has, and its Objects on Path records.
|
|
pub(super) fn document(&mut self, o: &Obj) {
|
|
variable_library(o, 0, &mut self.variables);
|
|
self.constraint_data(o);
|
|
let Some(list) = find_entry(o, "ArtboardArray", 0) else { return };
|
|
for (i, ab) in list.items().enumerate().take(1000) {
|
|
let (p1, p2) = (ab.nums("PositionPoint1"), ab.nums("PositionPoint2"));
|
|
let ([x0, y0, ..], [x1, y1, ..]) = (p1.as_slice(), p2.as_slice()) else { continue };
|
|
let r = Rect::new(*x0, *y0, *x1, *y1).abs();
|
|
if !sane(r) || r.area() <= 0.0 {
|
|
continue;
|
|
}
|
|
let name = ab.text("Name").unwrap_or_else(|| format!("Artboard {}", i + 1));
|
|
self.artboards.push((name, r));
|
|
}
|
|
}
|
|
|
|
// ---- the document ----------------------------------------------------------------------
|
|
|
|
/// The artboard of a file without artboards or crop marks: its size around the template
|
|
/// box's centre (half a point off the grid).
|
|
fn sized_board(&self) -> Option<Rect> {
|
|
let ((w, h), (cx, cy)) = (self.art_size?, self.template_center?);
|
|
// Rounded out, but not off a whole point the decimals in the file only just miss.
|
|
let near = |v: f64| (v - v.round()).abs() < 1e-6;
|
|
let (left, top) = (cx - w / 2.0, cy + h / 2.0);
|
|
let x0 = if near(left) { left.round() } else { left.floor() };
|
|
let y1 = if near(top) { top.round() } else { top.ceil() };
|
|
let r = Rect::new(x0, y1 - h, x0 + w, y1);
|
|
(sane(r) && w > 0.0 && h > 0.0).then_some(r)
|
|
}
|
|
|
|
/// The first artboard: the `/Document`'s first, else the crop marks, else [`Self::sized_board`].
|
|
fn first_board(&self) -> Option<Rect> {
|
|
self.artboards.first().map(|(_, r)| *r).or(self.cropmarks).or_else(|| self.sized_board())
|
|
}
|
|
|
|
/// The page the art is compared with in the document's space, once the art has started.
|
|
pub(super) fn page_in_doc(&self) -> Option<Rect> {
|
|
let board = self.first_board().or_else(|| self.bbox_board())?;
|
|
Some(self.to_doc?.transform_rect_bbox(board))
|
|
}
|
|
|
|
/// The page of a file that says nothing of its artboard (the legacy format as other apps write
|
|
/// it): its art's bounding box.
|
|
fn bbox_board(&self) -> Option<Rect> {
|
|
let [x0, y0, x1, y1] = self.hires.or(self.bbox)?;
|
|
Some(Rect::new(x0, y0, x1, y1)).filter(|r| r.width() > 0.0 && r.height() > 0.0)
|
|
}
|
|
|
|
/// Art space → the document (fixed when the art starts, from the artboards read by then). A
|
|
/// Large Canvas editing copy is written at 1/N scale ([`Self::large_canvas`]): the factor makes
|
|
/// its art and boxes reach the page's size, which is what the page it is opened beside holds.
|
|
pub(super) fn ensure_space(&mut self) -> Affine {
|
|
if let Some(m) = self.to_doc {
|
|
return m;
|
|
}
|
|
let first = self.first_board().or_else(|| self.bbox_board());
|
|
let top_left = first.map_or(Point::new(0.0, 792.0), |r| Point::new(r.x0, r.y1));
|
|
let s = self.large_canvas.unwrap_or(1.0);
|
|
let m = Affine::new([s, 0.0, 0.0, -s, -s * top_left.x, s * top_left.y]);
|
|
self.to_doc = Some(m);
|
|
m
|
|
}
|
|
|
|
/// Art space → where the art being read goes: the document, or while a pattern's definition is
|
|
/// read, the pattern's space (y down).
|
|
pub(crate) fn space(&mut self) -> Affine {
|
|
if self.pattern.is_some() { FLIP } else { self.ensure_space() }
|
|
}
|
|
|
|
/// The swatches of the palette, `[None]` first, in its order, and its color groups; a gradient
|
|
/// swatch only when its gradient was read, and a pattern swatch only for a pattern in
|
|
/// `patterns`. `None` when the palette is empty.
|
|
fn palette_swatches(&mut self, patterns: &[PatternDef]) -> Option<(Vec<Swatch>, Vec<vectorcraft_color::swatch::SwatchGroup>)> {
|
|
if self.palette.is_empty() {
|
|
return None;
|
|
}
|
|
let palette = std::mem::take(&mut self.palette);
|
|
let swatch = |e: crate::import::ai::palette::Entry| -> Option<Swatch> {
|
|
use crate::import::ai::palette::Cell;
|
|
let (paint, global, spot) = match e.cell {
|
|
Cell::Color { color, global, spot } => (Paint::solid(color), global, spot),
|
|
Cell::Gradient(g) => {
|
|
let mut gp = vectorcraft_color::gradient::GradientPaint::new(self.gradients.get(&g)?.clone());
|
|
gp.swatch = Some(e.name.clone());
|
|
(Paint::Gradient(Box::new(gp)), false, false)
|
|
}
|
|
Cell::Pattern(p) => {
|
|
patterns.iter().find(|d| d.name == p)?;
|
|
(Paint::Pattern { pattern: p, xf: Affine::IDENTITY }, false, false)
|
|
}
|
|
};
|
|
Some(Swatch { name: e.name, paint, global, spot })
|
|
};
|
|
let mut swatches = vec![Swatch { name: "[None]".into(), paint: Paint::None, global: false, spot: false }];
|
|
swatches.extend(palette.swatches.into_iter().filter_map(swatch));
|
|
let groups: Vec<_> = palette
|
|
.groups
|
|
.into_iter()
|
|
.map(|(name, s)| vectorcraft_color::swatch::SwatchGroup { name, swatches: s.into_iter().filter_map(swatch).collect() })
|
|
.collect();
|
|
Some((swatches, groups))
|
|
}
|
|
|
|
pub(super) fn finish(mut self) -> Result<Structure, String> {
|
|
// Envelopes whose plugin never closed them keep their drawn art.
|
|
if !self.envelopes.is_empty() {
|
|
self.envelopes.clear();
|
|
self.warn(crate::import::ai::envelope::DRAWN);
|
|
}
|
|
if !self.unknown.is_empty() {
|
|
let ops: Vec<String> = self.unknown.iter().map(|o| format!("`{o}`")).collect();
|
|
self.unsupported.insert(format!("operators VectorCraft doesn't know ({})", ops.join(", ")));
|
|
}
|
|
if !self.unsupported.is_empty() {
|
|
let list: Vec<&str> = self.unsupported.iter().map(String::as_str).collect();
|
|
return Err(format!("it has {}, which VectorCraft doesn't read from it yet", join_and(&list)));
|
|
}
|
|
if self.doc.layers.is_empty() {
|
|
return Err("it has no layers".into());
|
|
}
|
|
let to_doc = self.ensure_space();
|
|
let artboard = self.first_board();
|
|
drop_commented_copies(&mut self.doc.layers, &self.hidden_ids);
|
|
super::on_path::apply(&mut self.doc, &self.on_path);
|
|
let mut doc = std::mem::replace(&mut self.doc, Document::new(1.0, 1.0));
|
|
let boards: Vec<(String, Rect)> = if self.artboards.is_empty() {
|
|
artboard.or_else(|| self.bbox_board()).map(|r| vec![("Artboard 1".to_string(), r)]).unwrap_or_default()
|
|
} else {
|
|
self.artboards.clone()
|
|
};
|
|
let board_rects: Vec<Rect> = boards.iter().map(|(_, r)| *r).collect();
|
|
doc.artboards = boards
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, (name, r))| Artboard {
|
|
id: i as u32 + 1,
|
|
name,
|
|
rect: to_doc.transform_rect_bbox(r),
|
|
locked: false,
|
|
show_center_mark: false,
|
|
show_cross_hairs: false,
|
|
show_video_safe_areas: false,
|
|
video_ruler_pixel_aspect_ratio: 1.0,
|
|
color: None,
|
|
})
|
|
.collect();
|
|
if doc.artboards.is_empty() {
|
|
let rect = doc.art_bounds().filter(|r| sane(*r) && r.area() > 0.0).unwrap_or(Rect::new(0.0, 0.0, 612.0, 792.0));
|
|
doc.artboards = vec![Artboard {
|
|
id: 1,
|
|
name: "Artboard 1".into(),
|
|
rect,
|
|
locked: false,
|
|
show_center_mark: false,
|
|
show_cross_hairs: false,
|
|
show_video_safe_areas: false,
|
|
video_ruler_pixel_aspect_ratio: 1.0,
|
|
color: None,
|
|
}];
|
|
}
|
|
if !self.ruler_guides.is_empty() {
|
|
let ids: Vec<u32> = doc.artboards.iter().map(|a| a.id).collect();
|
|
let (candidates, centre) = (std::mem::take(&mut self.ruler_guides), self.template_center);
|
|
let pick = |id: NodeId| -> Option<Guide> {
|
|
let (a, b) = candidates.get(&id)?;
|
|
let (vertical, board) = ruler_guide(*a, *b, centre, &board_rects)?;
|
|
let p = to_doc * *a;
|
|
let artboard = match board {
|
|
Some(i) => Some(*ids.get(i)?),
|
|
None => None,
|
|
};
|
|
let pos = if vertical { p.x } else { p.y };
|
|
pos.is_finite().then_some(Guide { vertical, pos, artboard, layer: None })
|
|
};
|
|
take_ruler_guides(&mut doc.layers, &pick, &mut doc.guides, 0);
|
|
}
|
|
if self.color_model.unwrap_or(self.cmyk_colors > self.rgb_colors) {
|
|
doc.color_mode = ColorMode::Cmyk;
|
|
let (swatches, groups) = vectorcraft_color::default_swatches(ColorMode::Cmyk.model());
|
|
(doc.swatches, doc.swatch_groups) = (swatches.into(), groups.into());
|
|
}
|
|
if let Some(u) = self.ruler_units.and_then(ruler_unit) {
|
|
doc.units = u;
|
|
}
|
|
// The file's own swatches in place of the defaults, in its order and colour groups.
|
|
if let Some((swatches, groups)) = self.palette_swatches(&doc.patterns) {
|
|
(doc.swatches, doc.swatch_groups) = (swatches.into(), groups.into());
|
|
}
|
|
// The colour operators' definitions first: a gradient, defined before the art, names a
|
|
// global process colour as a stop and would otherwise make it an ink (#1771).
|
|
for n in self.spots.iter().chain(&self.stop_spots) {
|
|
if doc.swatch(&n.name).is_none() {
|
|
doc.swatches.push(Swatch { name: n.name.clone(), paint: Paint::solid(n.color), global: true, spot: n.spot });
|
|
}
|
|
}
|
|
for name in doc.patterns.iter().map(|p| p.name.clone()).collect::<Vec<_>>() {
|
|
if doc.swatch(&name).is_none() {
|
|
doc.swatches.push(Swatch { paint: Paint::Pattern { pattern: name.clone(), xf: Affine::IDENTITY }, name, global: false, spot: false });
|
|
}
|
|
}
|
|
let mut warnings = self.warnings;
|
|
if self.drawn_looks > 0 {
|
|
warnings.push(format!(
|
|
"objects with several fills or strokes, effects or brushes came in as their drawn look ({}): their appearance isn't editable yet",
|
|
self.drawn_looks
|
|
));
|
|
}
|
|
warnings.extend(self.variables.note());
|
|
Ok(Structure {
|
|
doc,
|
|
to_doc,
|
|
bbox: self.hires.or(self.bbox),
|
|
artboard,
|
|
template: self.template,
|
|
hidden_unread: self.hidden_unread,
|
|
unread: self.unread,
|
|
unread_elsewhere: self.unread_elsewhere,
|
|
slot_names: self.slot_names,
|
|
restyled: self.restyled,
|
|
placed_elsewhere: self.placed_elsewhere,
|
|
envelopes_drawn: self.envelopes_drawn,
|
|
warnings,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The dictionary of entry `key` in `o` or the dictionaries it holds.
|
|
/// How far (points) a ruler guide's ends may lie from the canvas's or an artboard's edges.
|
|
const GUIDE_TOLERANCE: f64 = 0.01;
|
|
|
|
/// Whether the straight guide path from `a` to `b` (art space) is a ruler guide → whether it is
|
|
/// vertical, and the index in `boards` of the artboard it belongs to (`None`: a canvas guide). A
|
|
/// canvas guide runs across the whole canvas, 16,383 points centred on the template box's
|
|
/// centre `centre`; an artboard guide runs from edge to edge of one artboard of `boards`, inside it.
|
|
fn ruler_guide(a: Point, b: Point, centre: Option<(f64, f64)>, boards: &[Rect]) -> Option<(bool, Option<usize>)> {
|
|
let near = |u: f64, v: f64| (u - v).abs() <= GUIDE_TOLERANCE;
|
|
let vertical = match (near(a.x, b.x), near(a.y, b.y)) {
|
|
(true, false) => true,
|
|
(false, true) => false,
|
|
_ => return None,
|
|
};
|
|
let (along, across) = if vertical { ((a.y.min(b.y), a.y.max(b.y)), a.x) } else { ((a.x.min(b.x), a.x.max(b.x)), a.y) };
|
|
if let Some((cx, cy)) = centre {
|
|
let c = if vertical { cy } else { cx };
|
|
if near(along.0, c - CANVAS_CENTRE) && near(along.1, c + CANVAS_CENTRE) {
|
|
return Some((vertical, None));
|
|
}
|
|
}
|
|
let mut on = boards.iter().enumerate().filter(|(_, r)| {
|
|
let (ends, sides) =
|
|
if vertical { ((r.min_y(), r.max_y()), (r.min_x(), r.max_x())) } else { ((r.min_x(), r.max_x()), (r.min_y(), r.max_y())) };
|
|
near(along.0, ends.0) && near(along.1, ends.1) && (sides.0..=sides.1).contains(&across)
|
|
});
|
|
match (on.next(), on.next()) {
|
|
(Some((i, _)), None) => Some((vertical, Some(i))),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Moves the children of the layers and sublayers of `nodes` that `pick` makes a guide of into
|
|
/// `guides`, each on its layer.
|
|
fn take_ruler_guides(nodes: &mut [Arc<Node>], pick: &dyn Fn(NodeId) -> Option<Guide>, guides: &mut Vec<Guide>, depth: usize) {
|
|
if depth > MAX_DEPTH {
|
|
return;
|
|
}
|
|
for n in nodes {
|
|
if !matches!(n.kind, NodeKind::Layer { .. }) {
|
|
continue;
|
|
}
|
|
let id = n.id;
|
|
let Some(kids) = Arc::make_mut(n).children_mut() else { continue };
|
|
kids.retain(|k| match pick(k.id) {
|
|
Some(g) => {
|
|
guides.push(Guide { layer: Some(id), ..g });
|
|
false
|
|
}
|
|
None => true,
|
|
});
|
|
take_ruler_guides(kids, pick, guides, depth + 1);
|
|
}
|
|
}
|
|
|
|
pub(super) fn find_entry<'o>(o: &'o Obj, key: &str, depth: usize) -> Option<&'o Obj> {
|
|
if depth > 8 {
|
|
return None;
|
|
}
|
|
o.obj(key).or_else(|| o.entries.iter().flat_map(|(_, v)| v.iter()).filter_map(V::obj).find_map(|c| find_entry(c, key, depth + 1)))
|
|
}
|
|
|
|
/// Add the variables and data sets of the variable libraries in `o` or the dictionaries it holds
|
|
/// (see [`VariableLibrary`]) to `found`.
|
|
fn variable_library(o: &Obj, depth: usize, found: &mut VariableLibrary) {
|
|
if depth > MAX_DEPTH {
|
|
return;
|
|
}
|
|
if o.ty == "XMLNode" && element_name(o).as_deref() == Some("variableSets") {
|
|
for set in elements(o, "variableSet") {
|
|
found.variables += elements(set, "variables").flat_map(|v| elements(v, "variable")).count();
|
|
found.data_sets += elements(set, "sampleDataSets").flat_map(|d| elements(d, "sampleDataSet")).count();
|
|
}
|
|
return;
|
|
}
|
|
for c in o.items() {
|
|
variable_library(c, depth + 1, found);
|
|
}
|
|
}
|