Place: Link keeps a placed PDF or .ai page linked, as it does images

The File Handling preference Link Placed PDF and .ai Files
(linkPlacedPdf) is removed. File › Place with Link now places a page of
a PDF, .ai or .ait file read as PDF pages as a linked placed document,
as it links a raster image, and link: false embeds the page. Link is on
by default in the Place dialog and in file.place, so a PDF or .ai page
placed from a file, which used to be embedded, is now linked unless
Link is off. A password or textAs: "outlines" still embeds, and so do
EPS and PostScript .ai files.

The Place dialog's Link tooltip describes a PDF or .ai file as a
document.

A .ai file read from its editing data (its PDF part only a placeholder)
finds the files its art places by name, beside it. Package now copies
those files into the same folder as the .ai file, and warns when one has
to be copied under another name.
This commit is contained in:
dotCooCoo
2026-10-22 21:52:23 +07:00
committed by bflatastic
parent 3a1d407f1a
commit 781e62b9cc
35 changed files with 155 additions and 96 deletions
+35 -7
View File
@@ -148,7 +148,20 @@ impl<'a> Collector<'a> {
self.warnings.push(message);
}
fn collect(&mut self, doc: &mut Document, source: &str, destination: &str, depth: usize) -> Result<()> {
/// Warns when a file that placed `.ai` file `source` links (`name`) was copied as `copy`: the
/// copy of `source` finds it only by its name, beside it.
fn renamed(&mut self, lines: &mut Vec<String>, source: &str, path: &str, name: &str, copy: &str) {
if Path::new(copy).file_name().and_then(|n| n.to_str()) != Some(name) {
let message = format!(
"{path}: copied as {copy}; the copy of {source} looks for it as {name} and shows the file of that name, if any, else its preview"
);
self.warn(lines, message);
}
}
/// Collects the files `doc` links. `by_name`: `doc` is a placed `.ai` file's editing data,
/// whose links are not rewritten, so its copy finds them by name beside it.
fn collect(&mut self, doc: &mut Document, source: &str, destination: &str, depth: usize, by_name: bool) -> Result<()> {
// The caller checks cycles and depth before descending; active documents have
// a destination reserved so back-links can point at their single packaged copy.
self.visiting.insert(source.to_string(), destination.to_string());
@@ -183,8 +196,11 @@ impl<'a> Collector<'a> {
local.insert(file.path.clone(), Copied { name: ancestor, size: None, hash: None });
continue;
}
if let Some(existing) = self.copies.get(found_path) {
local.insert(file.path.clone(), existing.clone());
if let Some(existing) = self.copies.get(found_path).cloned() {
if by_name {
self.renamed(&mut lines, source, &file.path, &file.name, &existing.name);
}
local.insert(file.path.clone(), existing);
continue;
}
let Some(mut bytes) = file.bytes else {
@@ -199,8 +215,20 @@ impl<'a> Collector<'a> {
let name = safe_asset_name(&file.name);
let unique = free_name(&name, if self.opts.links_folder { &mut self.link_names } else { &mut self.taken });
let destination_file = format!("{dir}{unique}");
// A placed PDF or .ai file is copied as it is, as an image is.
if placed.contains(&file.path) && super::place::document::is_native(&file.name, &bytes) {
if by_name {
self.renamed(&mut lines, source, &file.path, &file.name, &destination_file);
}
// A placed PDF or .ai file is copied as it is, as an image is; the files a .ai file
// read from its editing data links are copied beside it.
let placed_file = placed.contains(&file.path);
if placed_file && let Some(mut art) = super::place::document::editing_data(&file.name, &bytes) {
if depth >= vectorcraft_doc::placed_document::MAX_DEPTH {
self.warn(&mut lines, format!("{}: nesting limit reached; copied unchanged without collecting its own links", file.path));
} else {
self.collect(&mut art, found_path, &destination_file, depth + 1, true)?;
}
}
if placed_file && super::place::document::is_native(&file.name, &bytes) {
if depth >= vectorcraft_doc::placed_document::MAX_DEPTH {
self.warn(&mut lines, format!("{}: nesting limit reached; copied unchanged without collecting its own links", file.path));
} else {
@@ -212,7 +240,7 @@ impl<'a> Collector<'a> {
let include_linked =
saved.doc.linked_only_images().iter().any(|key| saved.doc.images.get(key).is_some_and(|blob| !blob.is_proxy()));
let mut nested = saved.doc;
self.collect(&mut nested, found_path, &destination_file, depth + 1)?;
self.collect(&mut nested, found_path, &destination_file, depth + 1, false)?;
if self.opts.relink {
// Preserve auxiliary native-file content when rewriting.
let mut options = vectorcraft_format::SaveOptions::for_doc(&nested);
@@ -394,7 +422,7 @@ fn collect_and_write(package: Package, write: &mut dyn FnMut(&str, &[u8]) -> Res
doc = d;
}
let mut collector = Collector::new(&o, root.clone(), &doc_name);
collector.collect(&mut doc, &doc_path, &doc_name, 0)?;
collector.collect(&mut doc, &doc_path, &doc_name, 0, false)?;
let mut entries = std::mem::take(&mut collector.entries);
entries.insert(0, (doc_name.clone(), vectorcraft_format::save_file(&doc)));
let mut taken = std::mem::take(&mut collector.taken);
+17 -3
View File
@@ -1,6 +1,6 @@
//! Placed documents (`file.place` of a VectorCraft document from a file, with `link`; of a PDF or
//! `.ai` file too, with Preferences › File Handling › Link Placed PDF and .ai Files): one locked
//! object showing an artboard of the document or a page of the PDF, a [`PlacedDocument`].
//! Placed documents (`file.place` of a VectorCraft document, a PDF or a `.ai` file from a file,
//! with `link`): one locked object showing an artboard of the document or a page of the PDF, a
//! [`PlacedDocument`].
//!
//! The object keeps the file's bytes (in the document's images, like image data) and the link; the
//! art it shows is read from them the way placing the file without Link reads it
@@ -44,6 +44,20 @@ pub(crate) fn is_pdf(name: &str, bytes: &[u8]) -> bool {
fileio::detect(name, bytes).is_some_and(|f| matches!(f.id, "pdf" | "ai" | "ait")) && !vectorcraft_pdf::is_postscript(bytes)
}
/// The editing data of the `.ai` file `name` (`bytes`) when its PDF part is only a placeholder:
/// the document its pages are read from ([`super::pdf_art`]), with the files it links. `None` for
/// other files.
pub(crate) fn editing_data(name: &str, bytes: &[u8]) -> Option<Document> {
if !is_pdf(name, bytes) {
return None;
}
let read = fileio::page_document(bytes, 0, &fileio::LoadOptions::default());
match fileio::page_or_editing_data(name, bytes, None, read) {
Ok((doc, _, true)) => Some(doc),
_ => None,
}
}
/// The type the bytes of placed file `name` (`bytes`) are stored with: a PDF's format's, else
/// [`MIME`].
fn mime(name: &str, bytes: &[u8]) -> &'static str {
+5 -6
View File
@@ -13,9 +13,8 @@
//! preview ([`fileio::load`]).
//! - A PDF/.ai/.ait page or a native document's artboard becomes one group, clipped to the page
//! (`crop: "crop"`; a PDF page's `art`, `trim`, `bleed` or `media` box too) or bounded by its art
//! (`crop: "bounding"`). With Link and Preferences › File Handling › Link Placed PDF and .ai
//! Files on, a PDF/.ai/.ait page placed from a file becomes a placed document instead, as a
//! VectorCraft document does (below).
//! (`crop: "bounding"`). With Link, a PDF/.ai/.ait page placed from a file becomes a placed
//! document instead, as a VectorCraft document does (below).
//! - A text file becomes area type, decoded and cleaned up with the Text Import Options (`text`,
//! [`text`]).
//! - An RTF file becomes area type with its character and paragraph formatting ([`rtf`]).
@@ -64,7 +63,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Place…",
["File"],
Some("Cmd+Shift+P"),
"{path | name | dataBase64 (a file's path, or its name with its bytes), link?: true (a raster image keeps its file's path; a VectorCraft document at path becomes one locked placed document showing artboard `page`, linked to the file and read again when it changes (links.update; saving it in this app updates it at once), drawn as vectors in every output; so does a PDF/.ai page at path, showing its `crop` box, when the preference linkPlacedPdf is on (off by default) and neither password nor textAs: \"outlines\" is given; false: an editable copy of its art, as from dataBase64; other files are embedded), text?: {characterSet?: \"unicode\" (UTF-8, or UTF-16 with a byte-order mark; other bytes as the platform's 8-bit set) | \"ansi\" (the platform's 8-bit set), platform?: \"windows\" (Windows-1252) | \"mac\" (Mac Roman), removeLineReturns?: false (each block of lines becomes one paragraph; blank lines end paragraphs), removeParagraphReturns?: false (drop blank lines), replaceSpaces?: n (runs of n ≥ 2 spaces become a tab)} (a .txt file, placed as area type filling rect, the replaced object's bounds, or else the artboard less a 36 pt margin), template?: false (onto a new locked template layer below the current layer), replace?: false (swap the one selected object, keeping its stacking place and transform; no at/rect), at?: [x, y] centre (default: the first artboard's centre), rect?: [x, y, width, height] fit inside; aspect kept (wins over at), page?: 1 (PDF/.ai page, or a native document's artboard), crop?: \"crop\" (clipped to that page or artboard, default; an SVG, only when given: framed and clipped by its viewBox, else by its art's bounds) | \"bounding\" (the bounds of the art that shows, with its strokes and effects, as an export measures them: hidden objects and guides left out) | \"art\" | \"trim\" | \"bleed\" | \"media\" (a PDF page's boxes), password? (an encrypted PDF), dxf?: {…the DXF options of document.open} (a .dxf drawing: fit fills the artboard under at, else the first; fitted or uncentred art lands with its drawing's artboard on that artboard, else it is centred on at)} → {ids, name, format, linked, width, height, warnings}. Raster images come in at 100% of their physical size (the file's ppi, else 72); SVG, DXF, PDF/.ai, EMF/WMF (clipped to the picture's frame), EPS (clipped to its bounding box; as document.open reads it), a .ai saved without PDF compatibility (from its editing data, as document.open reads it: the art of the layers that show on artboard `page`, clipped to that artboard unless crop is \"bounding\") and native documents as one group, with the images, symbols, patterns and swatches they use. One undo step; selects what it placed (unless on a template layer); never touches the clipboard",
"{path | name | dataBase64 (a file's path, or its name with its bytes), link?: true (a raster image keeps its file's path; a VectorCraft document at path becomes one locked placed document showing artboard `page`, linked to the file and read again when it changes (links.update; saving it in this app updates it at once), drawn as vectors in every output; so does a PDF/.ai page at path, showing its `crop` box, unless password or textAs: \"outlines\" is given; false: an editable copy of its art, as from dataBase64; other files are embedded), text?: {characterSet?: \"unicode\" (UTF-8, or UTF-16 with a byte-order mark; other bytes as the platform's 8-bit set) | \"ansi\" (the platform's 8-bit set), platform?: \"windows\" (Windows-1252) | \"mac\" (Mac Roman), removeLineReturns?: false (each block of lines becomes one paragraph; blank lines end paragraphs), removeParagraphReturns?: false (drop blank lines), replaceSpaces?: n (runs of n ≥ 2 spaces become a tab)} (a .txt file, placed as area type filling rect, the replaced object's bounds, or else the artboard less a 36 pt margin), template?: false (onto a new locked template layer below the current layer), replace?: false (swap the one selected object, keeping its stacking place and transform; no at/rect), at?: [x, y] centre (default: the first artboard's centre), rect?: [x, y, width, height] fit inside; aspect kept (wins over at), page?: 1 (PDF/.ai page, or a native document's artboard), crop?: \"crop\" (clipped to that page or artboard, default; an SVG, only when given: framed and clipped by its viewBox, else by its art's bounds) | \"bounding\" (the bounds of the art that shows, with its strokes and effects, as an export measures them: hidden objects and guides left out) | \"art\" | \"trim\" | \"bleed\" | \"media\" (a PDF page's boxes), password? (an encrypted PDF), dxf?: {…the DXF options of document.open} (a .dxf drawing: fit fills the artboard under at, else the first; fitted or uncentred art lands with its drawing's artboard on that artboard, else it is centred on at)} → {ids, name, format, linked, width, height, warnings}. Raster images come in at 100% of their physical size (the file's ppi, else 72); SVG, DXF, PDF/.ai, EMF/WMF (clipped to the picture's frame), EPS (clipped to its bounding box; as document.open reads it), a .ai saved without PDF compatibility (from its editing data, as document.open reads it: the art of the layers that show on artboard `page`, clipped to that artboard unless crop is \"bounding\") and native documents as one group, with the images, symbols, patterns and swatches they use. One undo step; selects what it placed (unless on a template layer); never touches the clipboard",
has_doc,
place
),
@@ -141,7 +140,7 @@ pub(crate) fn pt_per_px(ppi: Option<(f64, f64)>) -> (f64, f64) {
/// The file `p` names, read into placeable art; a DXF drawing's Fit to Artboard fills `board`
/// (unless the params name their own box). A PDF/.ai page read from a path is placed linked with
/// Link when `link_pdf` (Preferences › File Handling › Link Placed PDF and .ai Files).
/// Link when `link_pdf` (false for the queries, which read it embedded).
fn load(p: &Value, cmd: &str, board: Option<Rect>, link_pdf: bool) -> Result<Loaded> {
let src = fileio::source(p, cmd)?;
let name = fileio::file_name(src.name);
@@ -474,7 +473,7 @@ fn place(s: &mut Session, p: &Value) -> Result<Value> {
(true, _) => return Err(bad(PLACE, "replace: select the one object to replace")),
};
let target = board_at(&st.doc, at);
let mut loaded = load(p, PLACE, target, s.prefs.link_placed_pdf)?;
let mut loaded = load(p, PLACE, target, true)?;
let old_node = old.and_then(|id| st.doc.node(id));
// Type reflows rather than scales: its frame takes the size it is placed at.
let is_text = matches!(loaded.art, Art::Text(_));
-1
View File
@@ -315,7 +315,6 @@ pub const PREF_SPECS: &[PrefSpec] = &[
p!("recoveryOffForComplex", "File Handling", "Data Recovery", "Turn off Data Recovery for complex documents", bool),
p!("recentFilesCount", "File Handling", "Files", "Number of Recent Files to Display", int(0, 30)),
p!("lowResProxyEps", "File Handling", "Files", "Use Low Resolution Proxy for Linked EPS", bool),
p!("linkPlacedPdf", "File Handling", "Files", "Link Placed PDF and .ai Files", bool),
p!("antiAliasedBitmaps", "File Handling", "Files", "Display Bitmaps as Anti-aliased Images in Pixel Preview", bool),
p!(
"updateLinks",
-5
View File
@@ -1034,10 +1034,6 @@ pub struct Prefs {
pub recovery_off_for_complex: bool,
pub recent_files_count: u32,
pub low_res_proxy_eps: bool,
/// Link Placed PDF and .ai Files (off by default): File › Place with Link places a page of a
/// PDF or `.ai` file as a linked placed document, as it places a VectorCraft document; off,
/// the page's art is embedded as one group.
pub link_placed_pdf: bool,
pub anti_aliased_bitmaps: bool,
pub update_links: String,
// Clipboard Handling
@@ -1257,7 +1253,6 @@ impl Default for Prefs {
recovery_off_for_complex: false,
recent_files_count: 20,
low_res_proxy_eps: false,
link_placed_pdf: false,
anti_aliased_bitmaps: false,
update_links: s("askWhenModified"),
copy_as_svg: true,
+2 -2
View File
@@ -448,7 +448,7 @@ fn a_placed_document_brings_its_links_with_the_files_pixels() {
/// The placed (linked) files of a `.ai` file come in with the preview the file keeps of them: one
/// not found shows it, one found is read at once into the same bounds, which isn't a change. A copy
/// saved without PDF compatibility, placed, reads the found file too.
/// saved without PDF compatibility, placed without Link, reads the found file too.
#[test]
fn links_of_an_imported_file_are_read_where_they_are_found() {
use vectorcraft_testkit::ai;
@@ -487,7 +487,7 @@ fn links_of_an_imported_file_are_read_where_they_are_found() {
let alone = dir.file("alone.ai");
write(&alone, &ai::ai(data.as_bytes(), "BT /F1 12 Tf 10 50 Td (Saved without its PDF part) Tj ET"));
let mut s = session();
s.execute("file.place", &json!({"path": alone})).unwrap();
s.execute("file.place", &json!({"path": alone, "link": false})).unwrap();
let (_, im) = placed(&s);
assert!((im.width, im.height) == (600, 300) && !s.doc().unwrap().doc.images[&im.key].is_proxy(), "{im:?}");
}
+65 -1
View File
@@ -415,7 +415,6 @@ fn package_copies_a_linked_pdf_page_as_it_is() {
let src = dir.file("art/flyer.pdf");
write(&src, &pdf);
let mut s = session();
run(&mut s, "prefs.set", json!({"key": "linkPlacedPdf", "value": true}));
let placed = run(&mut s, "file.place", json!({"path": src}));
assert_eq!(placed["linked"], true, "{placed}");
save(&mut s, &dir.file("poster.vectorcraft"));
@@ -430,6 +429,71 @@ fn package_copies_a_linked_pdf_page_as_it_is() {
assert!(s.doc().unwrap().doc.has_placed());
}
/// `dir/art/badge.ai`, saved without its PDF part, its art placing each of `pics` (600 × 300 px,
/// with a 2×1 preview) → its path.
fn ai_placing(dir: &Folder, pics: &[&str]) -> String {
use vectorcraft_testkit::ai;
let place = |pic: &str| {
let pic = pic.replace('\\', "/");
format!(
"%AI5_BeginPlace\n[0.1 0 0 -0.1 20 80] 0 0 600 300 4 4 0\n({pic})`\n%AI26_BeginPlacedObjectPreview\n2 1 8 32 17 \x7f\x10\x10\x60\x7f\x10\x10\x60\n%AI26_EndPlacedObjectPreview\n~\n%AI5_EndPlace\n"
)
};
let data = ai::editing_data(200.0, 100.0, &ai::layer("Art", &pics.iter().map(|p| place(p)).collect::<String>()));
let src = dir.file("art/badge.ai");
write(&src, &ai::ai(data.as_bytes(), "BT /F1 12 Tf 10 50 Td (Saved without its PDF part) Tj ET"));
src
}
#[test]
fn package_copies_the_files_a_linked_ai_files_editing_data_places_beside_it() {
let dir = Folder::new("package-placed-ai");
let pic = dir.file("art/photo.png");
write(&pic, &png(600, 300, RED));
let src = ai_placing(&dir, &[&pic]);
let mut s = session();
let placed = run(&mut s, "file.place", json!({"path": src}));
assert_eq!(placed["linked"], true, "{placed}");
save(&mut s, &dir.file("poster.vectorcraft"));
let r = run(&mut s, "file.package", json!({"folder": dir.file("out")}));
assert_eq!((r["links"].as_u64(), r["missingLinks"].clone(), r["warnings"].clone()), (Some(2), json!([]), json!([])), "{r}");
let root = dir.0.join("out/poster Folder");
assert_eq!(std::fs::read(root.join("Links/photo.png")).unwrap(), png(600, 300, RED));
// The copy of the .ai finds the picture beside it, not its preview.
std::fs::remove_dir_all(dir.0.join("art")).unwrap();
let copy = root.join("Links/badge.ai").to_string_lossy().into_owned();
let shows_the_file = || {
let bytes = std::fs::read(&copy).unwrap();
let a = cmd::place::document::document_art("badge.ai", &bytes, Some(&copy), 1, false, vectorcraft_doc::PageBox::Crop, "t").unwrap();
a.doc.images.values().any(|b| !b.is_proxy())
};
assert!(shows_the_file());
std::fs::remove_file(root.join("Links/photo.png")).unwrap();
assert!(!shows_the_file(), "the preview only");
}
#[test]
fn package_warns_when_a_file_a_linked_ai_file_places_is_copied_under_another_name() {
let dir = Folder::new("package-placed-ai-names");
let (one, two) = (dir.file("art/a/photo.png"), dir.file("art/b/photo.png"));
write(&one, &png(600, 300, RED));
write(&two, &png(600, 300, BLUE));
let src = ai_placing(&dir, &[&one, &two]);
let mut s = session();
run(&mut s, "file.place", json!({"path": src}));
save(&mut s, &dir.file("poster.vectorcraft"));
let r = run(&mut s, "file.package", json!({"folder": dir.file("out")}));
let warnings = r["warnings"].as_array().unwrap();
assert_eq!(r["links"].as_u64(), Some(3), "{r}");
assert!(warnings.len() == 1 && warnings[0].as_str().unwrap().contains("looks for it as photo.png and shows the file of that name"), "{r}");
// Moved, the copy of the .ai shows Links/photo.png for both, as the warning says.
std::fs::remove_dir_all(dir.0.join("art")).unwrap();
let copy = dir.0.join("out/poster Folder/Links/badge.ai").to_string_lossy().into_owned();
let bytes = std::fs::read(&copy).unwrap();
let a = cmd::place::document::document_art("badge.ai", &bytes, Some(&copy), 1, false, vectorcraft_doc::PageBox::Crop, "t").unwrap();
assert_eq!(a.doc.images.values().filter(|b| !b.is_proxy()).count(), 1);
}
#[test]
fn package_keeps_distinct_linked_documents_with_colliding_file_names() {
let dir = Folder::new("package-collisions");
+8 -23
View File
@@ -1,8 +1,7 @@
//! Placed documents: a VectorCraft document placed linked (`file.place`'s default for one read
//! from a file), drawn and output as vectors, saved with its preview, checked, updated and
//! relinked when the file changes, and turned into an editable copy (Break Link, Expand). A PDF
//! or `.ai` page places the same way with Preferences › File Handling › Link Placed PDF and .ai
//! Files on.
//! or `.ai` page places the same way.
use serde_json::{Value, json};
use vectorcraft_color::{Color, Paint};
@@ -462,39 +461,29 @@ fn pdf_source(path: &str, format: &str, pages: &[[u8; 3]], extra: usize) {
write(path, &cmd::fileio::encode_all(&d, format, &p).unwrap().files[0].1);
}
/// Preferences › File Handling › Link Placed PDF and .ai Files on.
fn link_pdfs(s: &mut Session) {
s.execute("prefs.set", &json!({"key": "linkPlacedPdf", "value": true})).unwrap();
}
#[test]
fn a_pdf_page_stays_embedded_without_the_preference() {
let dir = Folder::new("placed-pdf-default");
fn a_pdf_page_placed_without_link_is_embedded() {
let dir = Folder::new("placed-pdf-embedded");
let src = dir.file("flyer.pdf");
pdf_source(&src, "pdf", &[RED], 0);
let mut s = session();
assert_eq!(s.execute("prefs.get", &json!({"key": "linkPlacedPdf"})).unwrap(), json!(false), "off by default");
let r = s.execute("file.place", &json!({"path": src, "link": true, "at": [200, 150]})).unwrap();
let r = s.execute("file.place", &json!({"path": src, "link": false, "at": [200, 150]})).unwrap();
assert_eq!(r["linked"], false, "{r}");
let n = s.doc().unwrap().doc.node(NodeId(r["ids"][0].as_u64().unwrap())).unwrap();
assert_eq!(n.kind_label(), "Clip Group");
assert!(!s.doc().unwrap().doc.has_placed());
// With the preference on, Link off still embeds, and so does text asked for as outlines.
link_pdfs(&mut s);
for p in [json!({"path": src, "link": false}), json!({"path": src, "textAs": "outlines"})] {
let r = s.execute("file.place", &p).unwrap();
assert_eq!(r["linked"], false, "{p}: {r}");
}
// Text asked for as outlines is embedded with Link too.
let r = s.execute("file.place", &json!({"path": src, "textAs": "outlines"})).unwrap();
assert_eq!(r["linked"], false, "{r}");
assert!(!s.doc().unwrap().doc.has_placed());
}
#[test]
fn with_the_preference_a_pdf_page_places_linked() {
fn with_link_a_pdf_page_places_linked() {
let dir = Folder::new("placed-pdf-linked");
let src = dir.file("flyer.pdf");
pdf_source(&src, "pdf", &[RED], 0);
let mut s = session();
link_pdfs(&mut s);
let id = place(&mut s, &src);
let p = placed(&s, id);
assert_eq!((p.link.path.as_str(), p.link.page, p.bounding), (src.as_str(), Some(1), false), "{p:?}");
@@ -517,7 +506,6 @@ fn a_changed_pdf_is_modified_and_update_links_reads_it_again() {
let src = dir.file("flyer.pdf");
pdf_source(&src, "pdf", &[RED], 0);
let mut s = session();
link_pdfs(&mut s);
let id = place(&mut s, &src);
pdf_source(&src, "pdf", &[BLUE], 1);
assert_eq!(s.execute("links.check", &json!({})).unwrap()["links"][0]["status"], "modified");
@@ -546,7 +534,6 @@ fn a_linked_pdf_page_outputs_as_vectors_after_the_document_reopens() {
let src = dir.file("flyer.pdf");
pdf_source(&src, "pdf", &[RED], 3);
let mut s = session();
link_pdfs(&mut s);
let id = place(&mut s, &src);
let doc_path = dir.file("poster.vectorcraft");
save(&mut s, &doc_path);
@@ -572,7 +559,6 @@ fn saving_and_reopening_keeps_a_linked_pdf_pages_number_and_box() {
let src = dir.file("flyer.pdf");
pdf_source(&src, "pdf", &[RED, BLUE], 4);
let mut s = session();
link_pdfs(&mut s);
let r = s.execute("file.place", &json!({"path": src, "page": 2, "crop": "media", "at": [200, 150]})).unwrap();
assert_eq!(r["linked"], true, "{r}");
let id = NodeId(r["ids"][0].as_u64().unwrap());
@@ -604,7 +590,6 @@ fn a_pdf_compatible_ai_file_places_linked_like_a_pdf() {
let src = dir.file("badge.ai");
pdf_source(&src, "ai", &[RED], 0);
let mut s = session();
link_pdfs(&mut s);
let id = place(&mut s, &src);
assert_eq!(placed(&s, id).link.path, src);
assert!(near(centre_colour(&s.doc().unwrap().doc), RED));
-1
View File
@@ -3363,7 +3363,6 @@
Use Even-Odd Fill Rule Използване на правило за запълване четно-нечетно
Use Japanese Crop Marks Използване на японски марки за изрязване
Use Low Resolution Proxy for Linked EPS Използване на нискорезолютивно прокси за свързани EPS файлове
Link Placed PDF and .ai Files Свързване на поставените PDF и .ai файлове
Use Non-Zero Winding Fill Rule Използване на правило за запълване с ненулево навиване
Use Precise Cursors Използване на прецизни курсори
Use Pressure Pen Използване на чувствителна на натиск писалка
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3522,7 +3522,6 @@ theme Appearance Motiv
Use Compression Použít kompresi
Use Japanese Crop Marks Použít japonské ořezové značky
Use Low Resolution Proxy for Linked EPS Pro propojené EPS použít zástupce v nízkém rozlišení
Link Placed PDF and .ai Files Propojit umístěné soubory PDF a .ai
Use Precise Cursors Používat přesné kurzory
Use Preview Bounds Použít hranice náhledu
When pasting text Při vkládání textu
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3273,7 +3273,6 @@ theme Appearance Udseende
Use Compression Anvend komprimering
Use Japanese Crop Marks Anvend japanske beskæringsmærker
Use Low Resolution Proxy for Linked EPS Anvend proxy med lav opløsning til koblede EPS-filer
Link Placed PDF and .ai Files Kobl placerede PDF- og .ai-filer
Use Precise Cursors Anvend præcise markører
Use Preview Bounds Anvend visningsgrænser
When pasting text Når tekst indsættes
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3382,7 +3382,6 @@ effect Points Punkte
Use Even-Odd Fill Rule Gerade-Ungerade-Füllregel verwenden
Use Japanese Crop Marks Japanische Schnittmarken verwenden
Use Low Resolution Proxy for Linked EPS Niedrig aufgelösten Platzhalter für verknüpfte EPS-Dateien verwenden
Link Placed PDF and .ai Files Platzierte PDF- und .ai-Dateien verknüpfen
Use Non-Zero Winding Fill Rule Nicht-Null-Füllregel verwenden
Use Precise Cursors Präzise Cursor verwenden
Use Pressure Pen Stiftdruck verwenden
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3720,7 +3720,6 @@ theme Appearance Εμφάνιση
Use Even-Odd Fill Rule Χρήση κανόνα γεμίσματος άρτιου-περιττού
Use Japanese Crop Marks Χρήση ιαπωνικών σημαδιών περικοπής
Use Low Resolution Proxy for Linked EPS Χρήση αντιπροσώπου χαμηλής ανάλυσης για συνδεδεμένο EPS
Link Placed PDF and .ai Files Σύνδεση τοποθετημένων αρχείων PDF και .ai
Use Non-Zero Winding Fill Rule Χρήση κανόνα γεμίσματος μη μηδενικής περιέλιξης
Use Precise Cursors Χρήση δεικτών ακριβείας
Use Pressure Pen Χρήση πένας πίεσης
Can't render this file because it contains an unexpected character in line 2045 and column 19.
-1
View File
@@ -3429,7 +3429,6 @@
Use Even-Odd Fill Rule Usar regla de relleno par-impar
Use Japanese Crop Marks Usar marcas de corte japonesas
Use Low Resolution Proxy for Linked EPS Usar proxy de baja resolución para EPS vinculados
Link Placed PDF and .ai Files Vincular archivos PDF y .ai colocados
Use Non-Zero Winding Fill Rule Usar regla de relleno distinto de cero
Use Precise Cursors Usar cursores precisos
Use Pressure Pen Usar lápiz de presión
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3450,7 +3450,6 @@ theme Appearance Ulkoasu
Use Compression Käytä pakkausta
Use Japanese Crop Marks Käytä japanilaisia rajausmerkkejä
Use Low Resolution Proxy for Linked EPS Käytä matalan tarkkuuden välityskuvaa linkitetyille EPS-tiedostoille
Link Placed PDF and .ai Files Linkitä sijoitetut PDF- ja .ai-tiedostot
Use Precise Cursors Käytä tarkkoja kohdistimia
Use Preview Bounds Käytä esikatselurajoja
When pasting text Liitettäessä tekstiä
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3426,7 +3426,6 @@
Use Even-Odd Fill Rule Utiliser la règle de remplissage pair-impair
Use Japanese Crop Marks Utiliser les traits de coupe japonais
Use Low Resolution Proxy for Linked EPS Utiliser une image d’aperçu basse résolution pour les EPS liés
Link Placed PDF and .ai Files Lier les fichiers PDF et .ai importés
Use Non-Zero Winding Fill Rule Utiliser la règle de remplissage non nul
Use Precise Cursors Utiliser des pointeurs précis
Use Pressure Pen Utiliser la pression du stylet
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -4143,7 +4143,6 @@ mark-style Japanese Japán
Use Japanese Crop Marks Japán vágójelek használata
Use Library Item Könyvtárelem használata
Use Low Resolution Proxy for Linked EPS Kis felbontású helyettesítő kép csatolt EPS-hez
Link Placed PDF and .ai Files Elhelyezett PDF- és .ai-fájlok csatolása
Use Non-Zero Winding Fill Rule Nem nulla körüljárási kitöltési szabály
Use Precise Cursors Pontos kurzorok használata
Use Pressure Pen Nyomásérzékeny toll használata
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3273,7 +3273,6 @@ theme Appearance Tampilan
Use Compression Gunakan kompresi
Use Japanese Crop Marks Gunakan tanda potong Jepang
Use Low Resolution Proxy for Linked EPS Gunakan proksi resolusi rendah untuk EPS tertaut
Link Placed PDF and .ai Files Tautkan file PDF dan .ai yang ditempatkan
Use Precise Cursors Gunakan kursor presisi
Use Preview Bounds Gunakan batas pratinjau
When pasting text Saat menempel teks
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
3273
3274
3275
3276
3277
3278
-1
View File
@@ -3464,7 +3464,6 @@ mark-style Japanese Giapponese
Use Even-Odd Fill Rule Usa regola di riempimento pari-dispari
Use Japanese Crop Marks Usa segni di ritaglio giapponesi
Use Low Resolution Proxy for Linked EPS Usa proxy a bassa risoluzione per EPS collegati
Link Placed PDF and .ai Files Collega i file PDF e .ai inseriti
Use Non-Zero Winding Fill Rule Usa regola di riempimento diversa da zero
Use Precise Cursors Usa cursori di precisione
Use Pressure Pen Usa penna a pressione
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3112,7 +3112,6 @@ mark-style Japanese 日本式
Use Even-Odd Fill Rule 奇偶規則を使用
Use Japanese Crop Marks 日本式トンボを使用
Use Low Resolution Proxy for Linked EPS リンク EPS に低解像度プロキシを使用
Link Placed PDF and .ai Files 配置した PDF と .ai ファイルをリンク
Use Non-Zero Winding Fill Rule ノンゼロワインディング規則を使用
Use Precise Cursors 精密カーソルを使用
Use Pressure Pen 筆圧を使用
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3083,7 +3083,6 @@ theme Appearance იერსახე
Use Compression შეკუმშვის გამოყენება
Use Japanese Crop Marks იაპონური ჭრის ნიშნების გამოყენება
Use Low Resolution Proxy for Linked EPS დაბალი გარჩევადობის პროქსის გამოყენება დაკავშირებული EPS-ისთვის
Link Placed PDF and .ai Files განთავსებული PDF და .ai ფაილების დაკავშირება
Use Precise Cursors ზუსტი კურსორების გამოყენება
Use Preview Bounds გადახედვის საზღვრების გამოყენება
When pasting text ტექსტის ჩასმისას
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3106,7 +3106,6 @@ mark-style Japanese 일본식
Use Even-Odd Fill Rule 짝홀 칠 규칙 사용
Use Japanese Crop Marks 일본식 자르기 표시 사용
Use Low Resolution Proxy for Linked EPS 연결된 EPS에 저해상도 프록시 사용
Link Placed PDF and .ai Files 가져온 PDF 및 .ai 파일 연결
Use Non-Zero Winding Fill Rule 논제로 와인딩 칠 규칙 사용
Use Precise Cursors 정밀 커서 사용
Use Pressure Pen 필압 펜 사용
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3287,7 +3287,6 @@ theme Appearance Vormgeving
Use Compression Compressie gebruiken
Use Japanese Crop Marks Japanse aftekenlijnen gebruiken
Use Low Resolution Proxy for Linked EPS Proxy met lage resolutie gebruiken voor gekoppelde EPS
Link Placed PDF and .ai Files Geplaatste PDF- en .ai-bestanden koppelen
Use Precise Cursors Nauwkeurige cursors gebruiken
Use Preview Bounds Voorvertoningsgrenzen gebruiken
When pasting text Bij het plakken van tekst
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3384,7 +3384,6 @@ theme Appearance Wygląd
Use Compression Używaj kompresji
Use Japanese Crop Marks Używaj japońskich znaczników przycięcia
Use Low Resolution Proxy for Linked EPS Używaj podglądu niskiej rozdzielczości dla połączonych EPS
Link Placed PDF and .ai Files Łącz umieszczone pliki PDF i .ai
Use Precise Cursors Używaj precyzyjnych kursorów
Use Preview Bounds Używaj granic podglądu
When pasting text Przy wklejaniu tekstu
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -2797,7 +2797,6 @@
Use Even-Odd Fill Rule Usar Regra de Preenchimento Par-Ímpar
Use Japanese Crop Marks Usar Marcas de Corte Japonesas
Use Low Resolution Proxy for Linked EPS Usar Proxy de Baixa Resolução para EPS Vinculado
Link Placed PDF and .ai Files Vincular Arquivos PDF e .ai Inseridos
Use Non-Zero Winding Fill Rule Usar Regra de Preenchimento de Enrolamento Diferente de Zero
Use Precise Cursors Usar Cursores Precisos
Use Pressure Pen Usar Caneta de Pressão
Can't render this file because it contains an unexpected character in line 6 and column 47.
-1
View File
@@ -3460,7 +3460,6 @@ mark-style Japanese Японский
Use Even-Odd Fill Rule Использовать правило заливки «чёт-нечет»
Use Japanese Crop Marks Использовать японские метки обрезки
Use Low Resolution Proxy for Linked EPS Использовать низкоразрешённый прокси для связанных EPS
Link Placed PDF and .ai Files Связывать размещённые файлы PDF и .ai
Use Non-Zero Winding Fill Rule Использовать правило заливки «ненулевой»
Use Precise Cursors Использовать точные курсоры
Use Pressure Pen Использовать перо с нажимом
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -3363,7 +3363,6 @@
Use Even-Odd Fill Rule Använd fyllningsregeln jämn-udda
Use Japanese Crop Marks Använd japanska beskärningsmärken
Use Low Resolution Proxy for Linked EPS Använd proxy med låg upplösning för länkad EPS
Link Placed PDF and .ai Files Länka placerade PDF- och .ai-filer
Use Non-Zero Winding Fill Rule Använd fyllningsregeln nollskild lindning
Use Precise Cursors Använd exakta markörer
Use Pressure Pen Använd tryckkänslig penna
Can't render this file because it contains an unexpected character in line 5 and column 25.
-1
View File
@@ -3457,7 +3457,6 @@ effect Points Точки
Use Even-Odd Fill Rule Використовувати правило заливки «парне–непарне»
Use Japanese Crop Marks Використовувати японські мітки обрізання
Use Low Resolution Proxy for Linked EPS Використовувати попередній перегляд низької роздільної здатності для пов’язаних EPS
Link Placed PDF and .ai Files Пов’язувати розміщені файли PDF і .ai
Use Non-Zero Winding Fill Rule Використовувати правило заливки «ненульове заповнення»
Use Precise Cursors Використовувати точні курсори
Use Pressure Pen Використовувати перо з натиском
Can't render this file because it contains an unexpected character in line 733 and column 40.
-1
View File
@@ -3833,7 +3833,6 @@ mark-style Japanese Kiểu Nhật
Use Japanese Crop Marks Dùng dấu cắt kiểu Nhật
Use Library Item Dùng mục thư viện
Use Low Resolution Proxy for Linked EPS Dùng proxy độ phân giải thấp cho EPS liên kết
Link Placed PDF and .ai Files Liên kết tệp PDF và .ai đã đặt
Use Non-Zero Winding Fill Rule Dùng quy tắc tô quấn khác không
Use Outside Rectangle As Frame Dùng hình chữ nhật ngoài làm khung
Use Precise Cursors Dùng con trỏ chính xác
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
3833
3834
3835
3836
3837
3838
-1
View File
@@ -3303,7 +3303,6 @@ font-script Japanese 汉字
Use Even-Odd Fill Rule 使用奇偶填色规则
Use Japanese Crop Marks 使用日式裁切标记
Use Low Resolution Proxy for Linked EPS 链接 EPS 使用低分辨率代理
Link Placed PDF and .ai Files 链接置入的 PDF 和 .ai 文件
Use Non-Zero Winding Fill Rule 使用非零缠绕填色规则
Use Precise Cursors 使用精确光标
Use Pressure Pen 使用压感笔
Can't render this file because it contains an unexpected character in line 2 and column 73.
-1
View File
@@ -4223,7 +4223,6 @@ font-script Japanese 漢字
Use Even-Odd Fill Rule 使用奇偶填色規則
Use Japanese Crop Marks 使用日式裁切標記
Use Low Resolution Proxy for Linked EPS 連結的 EPS 使用低解析度代理
Link Placed PDF and .ai Files 連結置入的 PDF 和 .ai 檔案
Use Non-Zero Winding Fill Rule 使用非零纏繞填色規則
Use Precise Cursors 使用精確游標
Use Pressure Pen 使用壓感筆
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6 -9
View File
@@ -155,7 +155,6 @@ fn open_dialog(app: &mut VectorcraftApp, files: Vec<Value>) {
let units = app.session.active().map(|d| d.doc.units).unwrap_or_default();
let info: Vec<String> = files.iter().map(|f| summary(app, f, units)).collect();
let one_object = app.session.active().is_some_and(|d| d.selection.objects.len() == 1);
let pdf = app.session.prefs.link_placed_pdf;
app.ui.dialog = Some(crate::state::Dialog::new(
crate::dialogs::place::KIND,
json!({
@@ -164,20 +163,18 @@ fn open_dialog(app: &mut VectorcraftApp, files: Vec<Value>) {
"template": false,
"replace": false,
"__replace": files.len() == 1 && one_object,
"__documents": files.iter().any(|f| is_vectorcraft_file(f, pdf)),
"__others": !files.iter().all(|f| is_vectorcraft_file(f, pdf)),
"__documents": files.iter().any(is_vectorcraft_file),
"__others": !files.iter().all(is_vectorcraft_file),
"__info": info,
}),
));
}
/// Is dialog file entry `file` a VectorCraft document on disk (what Link places as one locked
/// object), or, with `pdf` (Preferences › File Handling › Link Placed PDF and .ai Files), a PDF or
/// `.ai` file on disk?
fn is_vectorcraft_file(file: &Value, pdf: bool) -> bool {
let native = ["vectorcraft", vectorcraft_format::LEGACY_EXTENSION, "vctemplate"];
let linked = |ext: &str| native.contains(&ext) || (pdf && matches!(ext, "pdf" | "ai" | "ait"));
file.get("path").and_then(Value::as_str).is_some_and(|p| linked(fileio::extension(p).as_str()))
/// object), or a PDF or `.ai` file on disk?
fn is_vectorcraft_file(file: &Value) -> bool {
let linked = ["vectorcraft", vectorcraft_format::LEGACY_EXTENSION, "vctemplate", "pdf", "ai", "ait"];
file.get("path").and_then(Value::as_str).is_some_and(|p| linked.contains(&fileio::extension(p).as_str()))
}
/// The engine params naming `file` (a dialog file entry).
+9 -9
View File
@@ -635,28 +635,28 @@ fn a_vectorcraft_document_places_linked_and_edit_original_opens_it() {
}
#[test]
fn with_link_placed_pdf_on_the_place_dialog_links_a_pdf_page() {
fn the_place_dialog_links_a_pdf_page_and_embeds_it_with_link_off() {
let mut app = app();
let mut src = Session::new();
src.execute("file.new", &json!({"width": 100, "height": 50})).unwrap();
src.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 50})).unwrap();
let pdf = src.execute("document.serialize", &json!({"format": "pdf", "preserveEditing": false})).unwrap();
let path = temp_file("flyer.pdf", &vectorcraft_format::base64_decode(pdf["dataBase64"].as_str().unwrap()).unwrap());
// Off (the default): Link's tooltip speaks of images, and the page is embedded.
// Link on (the default): one locked object, linked to the file.
place_picked(&mut app, &path);
let d = app.ui.dialog.as_ref().expect("the Place dialog");
assert!(d.bool("link") && !d.bool("__documents") && crate::dialogs::place::link_tip(d).contains("image file"));
crate::dialogs::confirm(&mut app).unwrap();
assert!(!app.session.active().unwrap().doc.has_placed());
app.run("prefs.set", json!({"key": "linkPlacedPdf", "value": true})).unwrap();
place_picked(&mut app, &path);
let d = app.ui.dialog.as_ref().unwrap();
assert!(d.bool("__documents") && !d.bool("__others"), "Link places it as one locked object");
assert!(d.bool("link") && d.bool("__documents") && !d.bool("__others"), "Link places it as one locked object");
assert!(crate::dialogs::place::link_tip(d).contains("locked object"));
crate::dialogs::confirm(&mut app).unwrap();
let st = app.session.active().unwrap();
let vectorcraft_doc::NodeKind::PlacedDocument(p) = &st.doc.node(st.selection.objects[0]).unwrap().kind else { panic!("not a placed document") };
assert_eq!(p.link.path, path);
// Link off: the page's art is embedded.
place_picked(&mut app, &path);
app.ui.dialog.as_mut().unwrap().fields.insert("link".into(), json!(false));
crate::dialogs::confirm(&mut app).unwrap();
let st = app.session.active().unwrap();
assert_eq!(st.doc.node(st.selection.objects[0]).unwrap().kind_label(), "Clip Group");
}
/// Tool key `key` as the Control bar's Apply (Enter) and Cancel (Escape) send it.
+7 -6
View File
@@ -2897,10 +2897,9 @@ unless `link: false`; a Photoshop document as its merged image), an SVG as one g
and swatches it uses. A VectorCraft document read from `path` is a placed document: one locked object showing its
artboard `page` (or, with `crop: "bounding"`, its art's bounds), linked to the file and read again when it changes
(see Linked images), and vectors in every output; with `link: false` (or from `dataBase64`) it is an editable copy
of its art, as one clipped group. With the File Handling preference `linkPlacedPdf` on (off by default), a PDF,
`.ai` or `.ait` page read from `path` with `link` (and neither `password` nor `textAs: "outlines"`) is a placed
document the same way, showing page `page` clipped to its `crop` box (or its art's bounds); with it off, the page
is placed as one clipped group. EPS and PostScript `.ai` files are embedded. `at` centres it, `rect` fits it, `replace`
of its art, as one clipped group. A PDF, `.ai` or `.ait` page read from `path` (with neither `password` nor
`textAs: "outlines"`) is a placed document the same way, showing page `page` clipped to its `crop` box (or its
art's bounds); with `link: false` it is placed as one clipped group. EPS and PostScript `.ai` files are embedded. `at` centres it, `rect` fits it, `replace`
swaps the selected object (keeping its place and transform), `template` puts it on a new template layer.
The art's bounds are the bounds of the art that shows, with its strokes and effects, as an export measures them:
hidden objects, guides and template layers do not count, and symbol instances are measured by their art. A page or artboard holds the objects
@@ -3169,13 +3168,15 @@ that includes a placed document fails and relinks nothing; placed documents are
files are. Find in Folder… in the Linked File Not Found dialog and in the Links panel menu (`ui.findLinksInFolder`) runs
both. The web build has no folder search.
Placed documents (a `.vectorcraft` file placed linked, or a PDF or `.ai` page placed linked with `linkPlacedPdf`)
Placed documents (a `.vectorcraft` file, or a PDF or `.ai` page, placed linked)
work the same way: the document keeps the file's bytes and a preview (JPEG or PNG, 1 px per point, at most 1024 px
a side), and a save writes the preview (Include Linked Files: the file); output reads the file again, or uses the
preview with a warning when it is gone or changed.
`links.list` and `links.info` give them `document: true`, the file's `format` (`VectorCraft`, `PDF`,
`PDF-compatible .ai`) and the artboard's or page's `pageWidth` and `pageHeight` (pt). A relinked PDF shows the same
page and box of the new file. File › Package copies a placed PDF or `.ai` file as it is.
page and box of the new file. File › Package copies a placed PDF or `.ai` file as it is. For a `.ai` file read from
its editing data, Package also copies the files its art places into the same folder. That file finds them by name,
and Package warns when one is copied under another name: the copy then shows the file of that name, or its preview.
Saving the file in the app updates the open documents that place it. `links.embed` (the Links panel's Break Link)
turns one into the editable copy placing without link gives, its symbols, patterns, swatches and images joining
the document; so does `object.expand`.
+1 -1
View File
@@ -854,7 +854,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- DXF control-point SPLINE import honors its closed flag (#1299 follow-up after #1320 fixed export), including rational curves; open curves whose endpoints meet stay open. Own-fixture regressions cover controls/knots, fit-point fallback and all export versions (`crates/cad/src/import/tests.rs`, `crates/cad/src/tests.rs`); native save/read checked. Circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE, and a non-circular ellipse keeps the existing fallback (`crates/cad/src/tests.rs`); CNC preset and VCarve QA remain pending.
- Affinity documents in (AF.1–AF.4): `.af` (Affinity 3) and `.afdesign`/`.afphoto`/`.afpub` (Affinity 1 and 2, container versions 8–12) open and place as editable art: layers and sublayers, groups (pass-through or isolated), artboards (with their names and backgrounds) and Publisher pages side by side, curves with live corners, parametric shapes (rectangles with corner radii, ellipses, polygons, stars, pies, triangles, trapezoids), compound shapes, solid/linear/elliptical gradient fills and strokes (weight, dashes, alignment, scale-with-object), clipping by nesting, vector and pixel masks, artistic and frame text with fonts, sizes, tracking, leading and alignment, placed images (their original JPEG/PNG) and pixel layers (8/16-bit RGB, CMYK), opacity, visibility, locking and blend modes, at the document's resolution in points. Effects, adjustments, brushes and pressure profiles, transparency gradients, master pages, symbol instances (imported as their cached picture), special shapes (cloud, heart, cog…, imported as ellipses) and Affinity-only blend modes are named in the import warning. A file whose native data can't be read opens as its embedded preview with the reason, and that preview can't be placed. Save never writes back over the Affinity file and there is no Affinity export. AF.5 (from DesignCraft's import, storytold/designcraft#291) adds master pages (on a locked layer behind the pages that show them), tables (area type per cell and a line per edge), each paragraph's alignment, indents, spacing and leading, horizontal text scale, outlined text, frame text sized by its text scale, drop shadows and outer glows as live effects (inner shadows as inner glows), outline effects as strokes, colour and gradient overlays baked into the paint, embedded Affinity documents as editable content (their artboards don't become artboards), pixel layers over 64 MP at a reduced resolution and HEIC photos from their stored pixels; automatic leading (size plus the font's line gap) fitted on thumbnails; underlines and strikethrough; strokes whose style is None (every shape stores one) left out, which also brings 6 public files closer to their thumbnails; line breaks inside a paragraph; table cell text centred as Affinity centres it, also where it is taller than its cell; CMYK pixel layers converted through the document's embedded CMYK profile (relative colorimetric with black-point compensation, as each layer's colour settings name), or kept as CMYK images when there is none; outline effects on stroked shapes around their stroke; artistic text whose paragraphs align differently aligned within its longest line, a frame's first baseline at cap height, and frames whose last lines' descenders hang out made tall enough to show them (all measured on a local document's own PDF export: baselines match to 0.1 pt); a local 56-artboard document's mean thumbnail difference went from 16.2 to 11.4, and its pages' mean difference from the owner's PDF export from 12.9 to 9.3. AF.4 adds Affinity 3 artboard recognition and actual-outline clipping, source-backed JPEG/PNG raster tiles, Unicode text-run recovery, stored fallback/exact installed fonts, OpenType feature flags and compound-geometry limits. 51 public CC0/MIT/Apache-2.0 files (33 current `.af`), pinned and sha256-verified (`cargo xtask corpus --affinity`), test native retention and every-board native/SVG/PDF/PSD output for current files; selected files compare their saved thumbnails. No Affinity reopen or independent paired PSD oracle was available; [source audit and complex-font gaps](affinity-validation.md); [scope, provenance and limits](../crates/affinity/README.md).
- Placed documents: File › Place of a `.vectorcraft` file links to it by default (`file.place`'s `link`): one locked object showing an artboard of the document (or its art's bounds), read the way placing it without Link reads it, its symbols, patterns and images kept apart from the document's; without Link (or from bytes, not a file) it is an editable copy as before. It stays vectors in every output (SVG, PDF, EPS, EMF, DXF), and so do placed documents inside it, read from their own files up to 8 levels deep (documents that place each other stop there). On screen they draw from bitmaps cached per file and zoom step, shared by every copy and made in the background (a placeholder until ready), so a sheet of them redraws in about a millisecond; exports and prints draw them exactly. Like linked images, a save keeps only a preview of each (JPEG when opaque) unless Include Linked Files; output, and zooming past the preview, read the file again, falling back to the preview with a warning when it is gone or changed. Saves for older versions write them as plain art. Links check, update, relink, Update Links on open, Link Info (the artboard's size), Placement Options, Break Link (also Embed, Object › Expand: the editable copy placing without Link gives, its resources joining the document), Edit Original (the document in a new tab) and the Links panel cover them; saving a document updates the open documents that place it. The native format (v4) keeps data of 8 KB or more after the JSON instead of base64 in it.
- Linked PDF and `.ai` pages: with Preferences › File Handling › Link Placed PDF and .ai Files on (`linkPlacedPdf`, off by default), File › Place with Link places a page of a PDF, `.ai` or `.ait` file read as PDF pages as a placed document, the page and its box (`crop`) kept with the link: Links check, update and relink it, Break Link and Object › Expand give the group placing it without Link gives, every output draws the page's vectors, and Package copies the file. With the preference off, or with a password or `textAs: "outlines"`, the page is embedded as before. EPS and PostScript `.ai` files are still embedded (`crates/engine/src/tests_placed_document.rs`).
- Linked PDF and `.ai` pages: File › Place with Link (the default) places a page of a PDF, `.ai` or `.ait` file read as PDF pages as a placed document, as it links an image, the page and its box (`crop`) kept with the link: Links check, update and relink it, Break Link and Object › Expand give the group placing it without Link gives, every output draws the page's vectors, and Package copies the file. Without Link, or with a password or `textAs: "outlines"`, the page is embedded. EPS and PostScript `.ai` files are still embedded (`crates/engine/src/tests_placed_document.rs`).
- File › Place with Bounding Box (`file.place {crop: "bounding"}`) measures the art that shows, with its strokes and effects, as an export does: hidden objects, guides and template layers do not count, a group whose only part on the page or artboard is hidden is not placed, and symbol instances are measured by their art, for `at`, `rect`, the place cursor and `file.place.info` (`crates/engine/src/tests_place.rs`).
- Desktop print transport uses `craft-print` for CUPS and modern Windows. Sheet composition, copies, separations and colour management remain in VectorCraft; Windows 7 retains WMI and the installed PDF viewer. The Print dialog's Media list also carries the Japanese papers — JIS B4 257 × 364 mm, JIS B5 182 × 257 mm and JIS B6 128 × 182 mm beside the ISO B4/B5 (250 × 353 and 176 × 250 mm, kept so settings and presets saved before them keep their paper), and はがき 100 × 148 mm, 往復はがき 148 × 200 mm, 封筒 長形3号 120 × 235 mm, 長形4号 90 × 225 mm, 角形2号 240 × 332 mm, 角形3号 216 × 277 mm and 四六判 127 × 188 mm; the JSON id or the dialog label names a paper, any case and spacing (`B5 (JIS)` or `b5jis`) (`crates/pdf/src/print/settings.rs`, `crates/pdf/src/tests_print.rs`). The print-preview and physical-device fidelity gaps remain, so print scores and hours are unchanged.
- A PDF, PDF-compatible `.ai` or `.ait` opened from its pages or its editing copy opens with the File Info that its XMP packet and its document information dictionary give: the author, description, keywords, copyright status, notice and URL, and the created and modified dates, the XMP packet's value where both give one; the title is not changed (`crates/pdf/src/tests_file_info.rs`, `crates/engine/src/tests_fileinfo.rs`).