diff --git a/crates/engine/src/cmd/fileio/commands.rs b/crates/engine/src/cmd/fileio/commands.rs index a3427bce3f..6a147a49cf 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), 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, 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 15a07e5aee..decd7ec35b 100644 --- a/crates/engine/src/cmd/fileio/pdfimport.rs +++ b/crates/engine/src/cmd/fileio/pdfimport.rs @@ -217,8 +217,11 @@ fn page_options(page: usize, o: &LoadOptions) -> ImportOptions { /// `document.open` with `linkPages`: the pages `o` picks of the PDF (or PDF-compatible `.ai` or /// `.ait`) `name`, read from `path`, as a document holding one placed document per page, linked to /// that page of the file as File › Place with Link places one ([`document::source`]). Each page -/// gets an artboard of its `o.crop` box (its art's bounds for `bounding`, a blank page's crop box), in a row from the left -/// with the gap a PDF's artboards open with, and a layer named after the page. The document opens +/// gets an artboard of its `o.crop` box (its art's bounds for `bounding`, a blank page's crop box; +/// 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 /// 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 @@ -247,6 +250,8 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa Some(range) => parse_range(range, count).map_err(|e| err(format!("pages: {e}")))?, }; let bounding = o.crop == CropTo::Bounding; + // A PDF-compatible `.ai` asked for its crop box opens on its trim boxes ([`LoadOptions::import_options`]). + let crop = if o.crop == CropTo::Crop && opened.has_illustrator_data() { CropTo::Trim } else { o.crop }; let mut doc = Document::new(1.0, 1.0); let board = doc.artboards.first().cloned().ok_or_else(|| err("a new document has no artboard"))?; doc.artboards.clear(); @@ -261,7 +266,11 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa for (i, &index) in picked.iter().enumerate() { let page = index as u32 + 1; let file_once = document::SharedFile { bytes: &shared, hash: &hash, opened: &opened }; - let read = |bounding| document::shared_source(name, file_once, Some(path), page, bounding, document::page_box(o.crop), C); + // On its trim box, a page shows its bleed box, the artboard on the trim box inside it, so + // the art in the bleed shows and prints as bleed. + let bleed = if crop == CropTo::Trim { opened.page_bleed(index) } else { None }; + 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) { Err(e) if bounding && e.to_string().contains("has no art to place") => (read(false)?, false), @@ -273,9 +282,14 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa return Err(bad(C, format!("linkPages: `{file}` isn't a PDF or a PDF-compatible .ai file"))); } 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]) { + *side = side.max(b.min(vectorcraft_doc::setup::MAX_BLEED)); + } 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, 0.0)); + p.xf = Affine::translate((x - left, -top)); } node.name = Some(file.clone()); let mut layer = Node::layer(doc.alloc_id(), &format!("Page {page}"), LayerColor::Preset((i % 27) as u8)); @@ -283,7 +297,7 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa children.push(Arc::new(node)); } doc.layers.push(Arc::new(layer)); - let rect = Rect::new(x, 0.0, x + s.frame.width(), s.frame.height()); + 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 { diff --git a/crates/engine/src/cmd/fileio/tests_aiimport.rs b/crates/engine/src/cmd/fileio/tests_aiimport.rs index 2aaa2b8a68..92374e63e7 100644 --- a/crates/engine/src/cmd/fileio/tests_aiimport.rs +++ b/crates/engine/src/cmd/fileio/tests_aiimport.rs @@ -1181,6 +1181,30 @@ fn link_pages_refuses_an_ai_file_saved_without_its_pdf_part() { let _ = std::fs::remove_dir_all(&d); } +/// `document.open` with `linkPages` opens a PDF-compatible `.ai` asked for its crop box on its trim +/// box, as it opens as art, showing its bleed box round the artboard, which is the document's bleed. +#[test] +fn link_pages_open_an_ai_file_on_its_trim_box_with_its_bleed() { + let d = std::env::temp_dir().join(format!("vc-ailinkpagestrim-{}", std::process::id())); + std::fs::create_dir_all(&d).unwrap(); + let path = d.join("trimmed.ai"); + let boxes = "/TrimBox [10 10 190 90] /BleedBox [4 2 196 98]"; + std::fs::write(&path, ai::ai_with(&compressed(&ai::sample_data()), "0 g 10 10 40 40 re f", boxes)).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; + let board = doc.artboards[0].rect; + assert_eq!((board.width(), board.height()), (180.0, 80.0), "the trim box"); + assert_eq!(doc.setup.bleed, [8.0, 8.0, 6.0, 6.0], "top, bottom, left, right"); + // The page shows its bleed box, so the art in the bleed is kept. + let placed = doc.layers[0].children().unwrap()[0].clone(); + let NodeKind::PlacedDocument(p) = &placed.kind else { panic!("{:?}", placed.kind) }; + assert_eq!((p.width, p.height, p.page_box), (192.0, 96.0, vectorcraft_doc::PageBox::Bleed)); + assert_eq!(p.xf.translation(), vectorcraft_geom::Vec2::new(-6.0, -8.0), "the trim box on the 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/crates/pdf/src/pages.rs b/crates/pdf/src/pages.rs index 013d09f22d..683a14d1c3 100644 --- a/crates/pdf/src/pages.rs +++ b/crates/pdf/src/pages.rs @@ -297,6 +297,17 @@ impl Opened { bleed, } } + + /// How far page `index`'s (0-based) bleed box reaches past its trim box, `[top, bottom, left, + /// right]` in points, as the page shows (rotated); `None` past the last page, and when the bleed + /// box doesn't hold the trim box. + pub fn page_bleed(&self, index: usize) -> Option<[f64; 4]> { + let page = self.pdf.pages().get(index)?; + let init = Affine::new(page.initial_transform(true).as_coeffs()); + let (trim, bleed) = (init.transform_rect_bbox(page_box(page, CropTo::Trim)), init.transform_rect_bbox(page_box(page, CropTo::Bleed))); + let sides = [trim.y0 - bleed.y0, bleed.y1 - trim.y1, trim.x0 - bleed.x0, bleed.x1 - trim.x1]; + sides.iter().all(|b| b.is_finite() && *b >= 0.0).then_some(sides) + } } /// A PDF's pages as an import on their trim boxes sizes them ([`Opened::trim_pages`]). diff --git a/docs/mcp.md b/docs/mcp.md index 265211778f..4a01284fd6 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1280,7 +1280,9 @@ such a page opens from that data at the page's size. `linkPages: true` (File › Open's Import PDF dialog: Import PDF pages as links) opens each page `pages` picks as a placed document linked to that page of the file, as `file.place` with `link` places one, instead of as editable art. 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`). The document opens in CMYK when most of those pages are painted mostly in +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 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