diff --git a/crates/engine/src/cmd/fileio/commands.rs b/crates/engine/src/cmd/fileio/commands.rs index 6a147a49cf..1710f57eb9 100644 --- a/crates/engine/src/cmd/fileio/commands.rs +++ b/crates/engine/src/cmd/fileio/commands.rs @@ -9,7 +9,7 @@ pub fn specs() -> Vec { "Open Document", [], None, - "{path | name | dataBase64 (a file's path, or its name with its bytes), pages?: \"2-3, 5\" (PDF/.ai: 1-based, default all; one artboard each) | page?: n, cropTo?: bounding|art|crop (default)|trim|bleed|media (the box each artboard gets; bounding: the art's bounds; trim: the bleed box round it becomes the document's bleed; a PDF-compatible .ai or a PDF saved with its editing data opens on its trim boxes for crop), password? (encrypted PDFs; see document.pdfInfo), textAs?: text (default: point type per line, lines wrapped in a frame as area type where they lay out the same, in the file's fonts by name; missing ones are listed in warnings) | outlines (glyph paths), layers?: true (default: optional content groups become layers with their visibility, print state and lock — art that is off comes in as a hidden layer; other art goes to a layer per page; a PSD file's layers become layers holding their pixels, with their names, visibility, opacity and blend modes, groups as layers of sublayers, layer masks as opacity masks, clipped layers and layers in blend modes VectorCraft doesn't have merged with the layers below) | false (one layer per page, without the art that is off; Save then asks for a name; a PSD file opens as its merged image), hiddenLayers?: false (a PSD file's hidden layers come in hidden instead of being left out), editingData?: true (default: an EPS, .ai or PDF that carries its editor's own copy of the art opens from it, with its layers, hidden objects, guides, artboards and the art outside them; a .ai or PDF whose type shows opens from its PDF part with textAs: outlines) | \"always\" (as true, and a .ai's editing copy is used even where it draws differently from its PDF part, which can be older; the warnings say so) | false (only what its page or PDF part prints, for print pipelines; a .ai saved without PDF compatibility then can't be opened), legacyNameEncoding?: windows-1252 (default) | macintosh | windows-949 (also euc-kr) | shift_jis | gbk | big5 (the code page names in the editing data of a .ai or an EPS are read in when they aren't UTF-8: layers, objects, artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables and data sets, and data sets' text values; warnings list the first three such names as read, and count the rest), colorMode?: rgb|cmyk (the mode the document opens in, its colours converted as file.documentColorMode does, grays? as there; default: the file's — a PDF keeps CMYK, Gray and spot inks (spot swatches at a tint) and opens in CMYK when painted mostly in CMYK), grays?: as file.documentColorMode, dxf?: {layout?: \"Model\" (default) | a paper layout's name (document.dxfInfo lists them), unit?, scale? (the ratio: 1 unit of the art = scale drawing units; default: the drawing at 1:1 in its own unit), fit?: false (scale to fit an artboard of fitTo?: [612, 792], turned to the art's orientation), scaleLineweights?: false (lineweights scale with the art), center?: true (false: the drawing's origin on the artboard's bottom-left corner; fitted art's bottom-left corner), mergeLayers?: false (all art on one layer)} (DXF), crop?: as cropTo (its older name), linkPages?: false (true: each page `pages` picks of a PDF or PDF-compatible .ai opens as a placed document linked to that page of the file, as file.place with link places one, on an artboard of its cropTo box (bounding: its art's bounds; a blank page: its crop box; a PDF-compatible .ai: its trim box for crop; on its trim box a page shows its bleed box round the artboard and gives the document its bleed), in a row, and a layer named after the page; links.embed makes a page editable art; refused for any other file, without a path, with a password or with textAs: outlines; Save then asks for a name), importer?: \"plugin.\" (an importer plug-in's format id, see document.formats: it opens the file, which must have an extension it lists, also one a built-in format reads), params?: {…} (the importer's own parameters, see plugin.info)} → {index, title, format, warnings, restored, missingLinks, modifiedLinks, updatedLinks: [{name, path, ids}]}; any readable format (see document.formats): .vectorcraft/.drawcraft, .svg/.svgz, .pdf/.ai, .ait, ASCII .dxf (each DXF layer with art a layer; blocks symbols; what can't come in is listed in warnings), PNG/JPEG/GIF/WebP/TIFF/BMP (an image opens as a document of its size at the resolution it declares, as file.place sizes it; 72 ppi, 1 px = 1 pt, when it declares none), .emf/.wmf (one artboard, the picture's frame; records VectorCraft doesn't read are skipped with one warning), PICT .pct/.pic/.pict (QuickDraw versions 1 and 2, with or without the file's 512-byte header: one artboard, the picture's frame; shapes, lines and regions as paths, text as point type, bitmaps, pixel maps and QuickTime JPEG images as images; what isn't read is listed in warnings), HP-GL plot files .plt/.hpgl/.hgl/.hpg (one artboard round the art; what isn't drawn is listed in warnings), .eps and PostScript .ai (one artboard, the bounding box: an EPS file VectorCraft wrote restores the document it carries, restored: true; other files are read by a PostScript interpreter — paths, colours and spot inks, clips, gradients, images, type in the fonts named — and come in as their TIFF preview, with a warning, when their PostScript can't be read). A PDF/.ai/.ait or SVG saved with Preserve Editing restores the native document it carries (restored: true; a PDF only when no pages are picked), unless the file was changed elsewhere since or the data can't be read: then its artwork is imported and the first warning says why. Templates (native templates, .ait) open as a new untitled document; a restored .ai keeps its path (Save writes .ai again) and opens on the artboard its editing copy names as active. A file that no built-in format reads, by its content or its extension, opens with the installed importer plug-in that lists its extension (the one whose id sorts first, when several do): it is given the file, at most 128 MiB, and its objects become a new document whose artboard is their bounds; Save then asks for a name, and the warnings are the importer's. Linked images are read from their files (looked for at their path, then relative to the document): missing ones show their saved preview (links.relink), modified ones are read again only with Preferences › Update Links: Automatically (else links.update). An SVG's files (relative links from the SVG's folder) stay linked, SVG files become art, missing ones a placeholder in their box (a warning and missingLinks). An SVG's variable data (SVG Options › Include Variable Data, a variableSets element) gives the document its text variables and data sets, each text variable bound to the type object its docRef names, unless the SVG restores the native document; variable data variable.loadLibrary would refuse is left out with a warning", + "{path | name | dataBase64 (a file's path, or its name with its bytes), pages?: \"2-3, 5\" (PDF/.ai: 1-based, default all; one artboard each) | page?: n, cropTo?: bounding|art|crop (default)|trim|bleed|media (the box each artboard gets; bounding: the art's bounds; trim: the bleed box round it becomes the document's bleed; a PDF-compatible .ai or a PDF saved with its editing data opens on its trim boxes for crop), password? (encrypted PDFs; see document.pdfInfo), textAs?: text (default: point type per line, lines wrapped in a frame as area type where they lay out the same, in the file's fonts by name; missing ones are listed in warnings) | outlines (glyph paths), layers?: true (default: optional content groups become layers with their visibility, print state and lock — art that is off comes in as a hidden layer; other art goes to a layer per page; a PSD file's layers become layers holding their pixels, with their names, visibility, opacity and blend modes, groups as layers of sublayers, layer masks as opacity masks, clipped layers and layers in blend modes VectorCraft doesn't have merged with the layers below) | false (one layer per page, without the art that is off; Save then asks for a name; a PSD file opens as its merged image), hiddenLayers?: false (a PSD file's hidden layers come in hidden instead of being left out), editingData?: true (default: an EPS, .ai or PDF that carries its editor's own copy of the art opens from it, with its layers, hidden objects, guides, artboards and the art outside them; a .ai or PDF whose type shows opens from its PDF part with textAs: outlines) | \"always\" (as true, and a .ai's editing copy is used even where it draws differently from its PDF part, which can be older; the warnings say so) | false (only what its page or PDF part prints, for print pipelines; a .ai saved without PDF compatibility then can't be opened), legacyNameEncoding?: windows-1252 (default) | macintosh | windows-949 (also euc-kr) | shift_jis | gbk | big5 (the code page names in the editing data of a .ai or an EPS are read in when they aren't UTF-8: layers, objects, artboards, colors, gradients, patterns, graphic styles, swatches, brushes, symbols, variables and data sets, and data sets' text values; warnings list the first three such names as read, and count the rest), colorMode?: rgb|cmyk (the mode the document opens in, its colours converted as file.documentColorMode does, grays? as there; default: the file's — a PDF keeps CMYK, Gray and spot inks (spot swatches at a tint) and opens in CMYK when painted mostly in CMYK), grays?: as file.documentColorMode, dxf?: {layout?: \"Model\" (default) | a paper layout's name (document.dxfInfo lists them), unit?, scale? (the ratio: 1 unit of the art = scale drawing units; default: the drawing at 1:1 in its own unit), fit?: false (scale to fit an artboard of fitTo?: [612, 792], turned to the art's orientation), scaleLineweights?: false (lineweights scale with the art), center?: true (false: the drawing's origin on the artboard's bottom-left corner; fitted art's bottom-left corner), mergeLayers?: false (all art on one layer)} (DXF), crop?: as cropTo (its older name), linkPages?: false (true: each page `pages` picks of a PDF or PDF-compatible .ai opens as a placed document linked to that page of the file, as file.place with link places one, on an artboard of its cropTo box (bounding: its art's bounds; a blank page: its crop box; a PDF-compatible .ai: its trim box for crop; on its trim box a page shows its bleed box round the artboard and gives the document its bleed), in a row 36 pt apart (on trim boxes, plus the bleed on both sides), and a layer named after the page; links.embed makes a page editable art; refused for any other file, without a path, with a password or with textAs: outlines; Save then asks for a name), importer?: \"plugin.\" (an importer plug-in's format id, see document.formats: it opens the file, which must have an extension it lists, also one a built-in format reads), params?: {…} (the importer's own parameters, see plugin.info)} → {index, title, format, warnings, restored, missingLinks, modifiedLinks, updatedLinks: [{name, path, ids}]}; any readable format (see document.formats): .vectorcraft/.drawcraft, .svg/.svgz, .pdf/.ai, .ait, ASCII .dxf (each DXF layer with art a layer; blocks symbols; what can't come in is listed in warnings), PNG/JPEG/GIF/WebP/TIFF/BMP (an image opens as a document of its size at the resolution it declares, as file.place sizes it; 72 ppi, 1 px = 1 pt, when it declares none), .emf/.wmf (one artboard, the picture's frame; records VectorCraft doesn't read are skipped with one warning), PICT .pct/.pic/.pict (QuickDraw versions 1 and 2, with or without the file's 512-byte header: one artboard, the picture's frame; shapes, lines and regions as paths, text as point type, bitmaps, pixel maps and QuickTime JPEG images as images; what isn't read is listed in warnings), HP-GL plot files .plt/.hpgl/.hgl/.hpg (one artboard round the art; what isn't drawn is listed in warnings), .eps and PostScript .ai (one artboard, the bounding box: an EPS file VectorCraft wrote restores the document it carries, restored: true; other files are read by a PostScript interpreter — paths, colours and spot inks, clips, gradients, images, type in the fonts named — and come in as their TIFF preview, with a warning, when their PostScript can't be read). A PDF/.ai/.ait or SVG saved with Preserve Editing restores the native document it carries (restored: true; a PDF only when no pages are picked), unless the file was changed elsewhere since or the data can't be read: then its artwork is imported and the first warning says why. Templates (native templates, .ait) open as a new untitled document; a restored .ai keeps its path (Save writes .ai again) and opens on the artboard its editing copy names as active. A file that no built-in format reads, by its content or its extension, opens with the installed importer plug-in that lists its extension (the one whose id sorts first, when several do): it is given the file, at most 128 MiB, and its objects become a new document whose artboard is their bounds; Save then asks for a name, and the warnings are the importer's. Linked images are read from their files (looked for at their path, then relative to the document): missing ones show their saved preview (links.relink), modified ones are read again only with Preferences › Update Links: Automatically (else links.update). An SVG's files (relative links from the SVG's folder) stay linked, SVG files become art, missing ones a placeholder in their box (a warning and missingLinks). An SVG's variable data (SVG Options › Include Variable Data, a variableSets element) gives the document its text variables and data sets, each text variable bound to the type object its docRef names, unless the SVG restores the native document; variable data variable.loadLibrary would refuse is left out with a warning", always, load::open ), diff --git a/crates/engine/src/cmd/fileio/pdfimport.rs b/crates/engine/src/cmd/fileio/pdfimport.rs index decd7ec35b..00eef89a11 100644 --- a/crates/engine/src/cmd/fileio/pdfimport.rs +++ b/crates/engine/src/cmd/fileio/pdfimport.rs @@ -221,7 +221,7 @@ fn page_options(page: usize, o: &LoadOptions) -> ImportOptions { /// a PDF-compatible `.ai` asked for its crop box gets its trim box, as it opens), in a row from the /// left with the gap a PDF's artboards open with, and a layer named after the page. On its trim box, /// a page shows its bleed box round the artboard, and the document's bleed is the pages' bleed (the -/// widest on each side). The document opens +/// widest on each side); the artboards are then the gap apart plus the bleed on both sides. The document opens /// in CMYK when most of the pages read in CMYK, unless `o.color_mode` names a mode. Refused without a /// path, for any other file, with a password (a linked page is read again without one), with /// `textAs: outlines` (a linked page shows the file's own type) and for a file over @@ -262,8 +262,11 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa let shared = Arc::new(bytes.to_vec()); // The file is hashed once for all its pages. let hash = link.hash.clone().unwrap_or_else(|| vectorcraft_doc::links::hash_bytes(bytes)); - let (mut x, mut warnings, mut cmyk_pages) = (0.0, Vec::::new(), 0); - for (i, &index) in picked.iter().enumerate() { + let (mut warnings, mut cmyk_pages) = (Vec::::new(), 0); + // Each page as read, with its bleed (top, bottom, left, right). The pages are laid out once + // the document's bleed is known. + let mut read_pages = Vec::with_capacity(picked.len()); + for &index in &picked { let page = index as u32 + 1; let file_once = document::SharedFile { bytes: &shared, hash: &hash, opened: &opened }; // On its trim box, a page shows its bleed box, the artboard on the trim box inside it, so @@ -272,7 +275,7 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa let page_box = if bleed.is_some() { vectorcraft_doc::PageBox::Bleed } else { document::page_box(crop) }; let read = |bounding| document::shared_source(name, file_once, Some(path), page, bounding, page_box, C); // A blank page has no art's bounds: it keeps its crop box, as opening it as art does. - let (s, bounding) = match read(bounding) { + let (mut s, bounding) = match read(bounding) { Err(e) if bounding && e.to_string().contains("has no art to place") => (read(false)?, false), s => (s?, bounding), }; @@ -283,10 +286,26 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa } cmyk_pages += usize::from(s.color_mode == ColorMode::Cmyk); // The document's bleed is the widest of the pages' on each side. - let [top, bottom, left, right] = bleed.unwrap_or_default(); - for (side, b) in doc.setup.bleed.iter_mut().zip([top, bottom, left, right]) { + let sides = bleed.unwrap_or_default(); + for (side, b) in doc.setup.bleed.iter_mut().zip(sides) { *side = side.max(b.min(vectorcraft_doc::setup::MAX_BLEED)); } + for w in std::mem::take(&mut s.warnings) { + if !warnings.contains(&w) { + warnings.push(w); + } + } + read_pages.push((page, s, bounding, sides)); + } + // Each artboard is the gap from the one before plus the bleed on both sides of it: the + // document's bleed, which export adds round every artboard, or a page's own bleed box where it + // reaches farther. Neither page then shows or exports the other's art. + let [_, _, doc_left, doc_right] = doc.setup.bleed; + let mut x = 0.0; + for (i, (page, s, bounding, [top, bottom, left, right])) in read_pages.into_iter().enumerate() { + if i > 0 { + x += doc_left.max(left); + } let mut node = document::node(&mut doc, &s, LinkInfo { page: Some(page), ..link.clone() }, bounding); if let NodeKind::PlacedDocument(p) = &mut node.kind { p.xf = Affine::translate((x - left, -top)); @@ -299,12 +318,7 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa doc.layers.push(Arc::new(layer)); let rect = Rect::new(x, 0.0, x + s.frame.width() - left - right, s.frame.height() - top - bottom); doc.artboards.push(Artboard { id: i as u32 + 1, name: format!("Artboard {}", i + 1), rect, ..board.clone() }); - x = rect.x1 + gap; - for w in s.warnings { - if !warnings.contains(&w) { - warnings.push(w); - } - } + x = rect.x1 + doc_right.max(right) + gap; } // Pages painted mostly in CMYK read as CMYK documents, as opening them as art does: the // document opens in CMYK (with the CMYK default swatches) when most of its pages do. diff --git a/crates/engine/src/cmd/fileio/tests_aiimport.rs b/crates/engine/src/cmd/fileio/tests_aiimport.rs index 92374e63e7..8b588975c4 100644 --- a/crates/engine/src/cmd/fileio/tests_aiimport.rs +++ b/crates/engine/src/cmd/fileio/tests_aiimport.rs @@ -1205,6 +1205,46 @@ fn link_pages_open_an_ai_file_on_its_trim_box_with_its_bleed() { let _ = std::fs::remove_dir_all(&d); } +#[test] +fn link_pages_on_their_trim_boxes_keep_the_bleed_between_the_artboards() { + // Two 100 pt trim boxes: the first with 60 pt of bleed on each side, the second with 80 pt on + // its left, more than the largest document bleed (72 pt). + let entries = format!("/PieceInfo << /Illustrator << /Private << /AIPrivateData1 {} 0 R /NumBlock 1 >> >> >>", first_extra(2)); + let first = PdfPage { + bleed: Some([0.0, 0.0, 220.0, 220.0]), + trim: Some([60.0, 60.0, 160.0, 160.0]), + entries, + ..PdfPage::new(220.0, 220.0, "0 g 0 0 220 220 re f") + }; + let second = PdfPage { + bleed: Some([0.0, 0.0, 240.0, 220.0]), + trim: Some([80.0, 60.0, 180.0, 160.0]), + ..PdfPage::new(240.0, 220.0, "0 g 0 0 240 220 re f") + }; + let d = std::env::temp_dir().join(format!("vc-ailinkpagesgap-{}", std::process::id())); + std::fs::create_dir_all(&d).unwrap(); + let path = d.join("pages.ai"); + std::fs::write(&path, pdf_with(&[first, second], &["<< /Length 4 >>\nstream\ndata\nendstream"], None)).unwrap(); + let path = path.to_string_lossy().to_string(); + let mut s = Session::new(); + s.execute("document.open", &json!({"path": path, "linkPages": true})).unwrap(); + let doc = &s.doc().unwrap().doc; + assert_eq!(doc.setup.bleed, [60.0, 60.0, 72.0, 60.0], "top, bottom, left, right"); + // The first page's bleed ends at 160; the second's starts 36 pt later, 80 pt left of its trim box. + let boards: Vec<(f64, f64)> = doc.artboards.iter().map(|a| (a.rect.x0, a.rect.x1)).collect(); + assert_eq!(boards, [(0.0, 100.0), (276.0, 376.0)]); + let shown: Vec = doc + .layers + .iter() + .map(|l| match &l.children().unwrap()[0].kind { + NodeKind::PlacedDocument(p) => p.xf.translation().x, + k => panic!("{k:?}"), + }) + .collect(); + assert_eq!(shown, [-60.0, 196.0], "each page's bleed box round its artboard"); + let _ = std::fs::remove_dir_all(&d); +} + /// A page of text alone, as text-only art has in its PDF part: no path is painted. const TEXT_ART: &str = "BT /F1 36 Tf 10 40 Td (Text only) Tj ET"; diff --git a/docs/mcp.md b/docs/mcp.md index 4a01284fd6..a100f0bad7 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1282,7 +1282,7 @@ placed document linked to that page of the file, as `file.place` with `link` pla Each page gets an artboard of its `cropTo` box (`bounding`: its art's bounds; a blank page gets its crop box), in a row from the left 36 pt apart, and a layer named after the page (`Page 2`). A PDF-compatible `.ai` with `cropTo: "crop"` gets its trim boxes, as it does when it opens as art. A page on its trim box shows its bleed box round the artboard, and the pages give the document -their bleed. The document opens in CMYK when most of those pages are painted mostly in +their bleed; the artboards are then 36 pt apart plus the bleed on both sides. The document opens in CMYK when most of those pages are painted mostly in CMYK, unless `colorMode` names a mode. All the pages' objects share one copy of the file. The Links panel lists each page, and `links.embed` makes a page editable art. `document.open` refuses `linkPages` for a file that is not a PDF or a PDF-compatible `.ai`, for a file given as