diff --git a/crates/engine/src/cmd/fileio/commands.rs b/crates/engine/src/cmd/fileio/commands.rs index 26272d873d..752dcbef42 100644 --- a/crates/engine/src/cmd/fileio/commands.rs +++ b/crates/engine/src/cmd/fileio/commands.rs @@ -36,7 +36,7 @@ pub fn specs() -> Vec { "Export Document", [], None, - "{path?, format?: svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|hpgl|tiff|bmp|tga|psd|vectorcraft|template (default: from the path's extension, else png; png8 writes an indexed .png; ait, a .ait template, is written by file.saveAsTemplate; an exporter plug-in's format is plugin., which an extension only it lists picks too), params?: {…} (an exporter plug-in's parameters; missing ones take their defaults, see plugin.info), selectedOnly?: false (the selected objects alone, in the layers and groups that hold them; an object of a blend, envelope, repeat or intertwine on its own), includeArtboardFill?: true (raster, PDF, SVG and EPS draw each exported artboard's background color behind its art, a transparent background too; false leaves it out), artboard?: 0, artboards?: [i…], rect?: [x, y, width, height] (raster formats: that area of the page in points, y down, instead of an artboard; not with artboard, artboards, range or useArtboards), range?: \"1-3, 5\" | \"all\" (1-based; PDF and .ai write one page per artboard, default all; SVG writes one file per artboard, {stem}-{artboard}.svg; raster formats write one artboard), useArtboards?: true (raster: one file per chosen artboard, default all, {stem}-{artboard}.{ext}; pdf/ai: every page) | false (pdf/ai/raster: the bounds of the visible art; SVG has it as an SVG option), ppi?: number (raster formats: pixels per inch, stored in the file; wins over scale; without either, the document's raster effects resolution), scale?: number (pixels per point), background?: transparent|white|black|\"#rrggbb\" (jpg: white when transparent; #rrggbbaa and #rgba are refused here and in matte), antiAlias?: none|art (default)|type (text snapped to pixels), interlaced?: false (png, Adam7), embedIcc?: true (png: an sRGB chunk marks the colours as sRGB), quality?: 90 (jpg: 0–100), colorModel?: rgb|cmyk|gray (jpg), method?: baseline|optimized|progressive, scans?: 3 (3–5, progressive), embedIcc?: true, imageMap?: none|client|server (an HTML or NCSA map of the objects with a URL, written as .html / .map), colors?: 256 (gif/png8: 2–256), reduction?: perceptual|selective (default)|adaptive|web|blackWhite|gray, dither?: none|diffusion (default)|pattern|noise, ditherAmount?: 100, transparency?: true, matte?: white|\"#rrggbb\"|none, interlaced?, lossless?: true (webp: lossy WebP isn't available yet: written lossless, with a warning), encoding?: utf8|utf16 (txt: the stories in stacking order, back to front, a thread once; utf16 with a byte order mark), lineEndings?: lf|crlf, selectionOnly?: false, svg?: {styling, outlineText, images, objectIds, decimals, minify, responsive, useArtboards, preserveEditing, metadata, fewerTspans, hiddenLayers, encoding, profile, embedFonts, variableData} (see document.formats), …the SVG options given flat instead, …the PDF options of document.exportPdf, …the DXF options of document.exportDxf (useArtboards: one drawing per artboard), …the EPS options of document.exportEps (useArtboards: one file per artboard, {stem}_{artboard}.eps, after a central {stem}.eps of the whole document when there are two or more and art shows; else the visible art), …emf and wmf write one picture of the artboard (useArtboards: true one file per chosen artboard, false the bounds of the visible art; EMF keeps curves, clipping, transparent images and gradients as images clipped to their shape; WMF flattens curves into polygons behind a placeable header; what a format leaves out comes back in warnings), …hpgl writes a plot of the artboard as pen moves for plotters and cutters (each visible path once along its line in plotter units; useArtboards: true one file per chosen artboard, false the bounds of the visible art), hpglVersion?: hpgl (default: IN, SP, PU, PD) | hpgl2 (also NP and PC pen colors, each pen's paths as one encoded polyline PE), curveTolerance?: 0.1 (points, 0.01 to 10), pens?: [\"#rrggbb\"…] (pen 1 first, up to 255; each path takes the nearest; default a pen per color in the order the art first draws in it, numbered alike in every file), origin?: bottomLeft (default) | center, sortPaths?: false (each pen's next path the nearest), insideFirst?: false (a path after the pen's paths that lie inside it), colorModel?: rgb|cmyk|gray (tiff: rgb keeps transparency as an alpha channel), lzw?: true, byteOrder?: little|big, embedIcc?: true, colorModel?: rgb|gray (bmp), depth?: 24 (bmp: 1 black and white, 4|8 a palette by reduction and dither, 16, 24, 32 with alpha), fileFormat?: windows|os2 (1, 4, 8 or 24 bits), rle?: false (4 and 8 bits, windows), flipRows?: false (top-down rows), depth?: 24 (tga: 16 one-bit alpha, 24, 32 with alpha), colorModel?: rgb|cmyk|gray (psd), layers?: true (each top-level layer a pixel layer with its opacity and blend mode; a background colour is a Background layer; false: one flat image, on white), maxEditability?: false (layers and sublayers become groups, each object a layer named as in the Layers panel, text by its text; clipping, masked or knockout layers stay one layer), hiddenLayers?: false (hidden layers and objects written hidden instead of left out), embedIcc?: true (at most 30000 pixels a side; formats without alpha are flattened on white)} → {path, format, bytes, warnings, files?: [path…] (several), linked?: [path…] (linked images, image maps)}; no path → {dataBase64, format, bytes, warnings, files?: [{name, dataBase64}], linked?: [{name, dataBase64}]}. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is ignored. Never changes the document's path", + "{path?, format?: svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|hpgl|tiff|bmp|tga|psd|vectorcraft|template (default: from the path's extension, else png; png8 writes an indexed .png; ait, a .ait template, is written by file.saveAsTemplate; an exporter plug-in's format is plugin., which an extension only it lists picks too), params?: {…} (an exporter plug-in's parameters; missing ones take their defaults, see plugin.info), selectedOnly?: false (the selected objects alone, in the layers and groups that hold them; an object of a blend, envelope, repeat or intertwine on its own), includeArtboardFill?: true (raster, PDF, SVG and EPS draw each exported artboard's background color behind its art, a transparent background too; false leaves it out), artboard?: 0, artboards?: [i…], rect?: [x, y, width, height] (raster formats: that area of the page in points, y down, instead of an artboard; not with artboard, artboards, range or useArtboards), range?: \"1-3, 5\" | \"all\" (1-based; PDF and .ai write one page per artboard, default all; SVG writes one file per artboard, {stem}-{artboard}.svg; raster formats write one artboard), useArtboards?: true (raster: one file per chosen artboard, default all, {stem}-{artboard}.{ext}; pdf/ai: every page) | false (pdf/ai/raster: the bounds of the visible art; pdf/ai: without editing data, with a warning, and ai refuses pdfCompatible: false; SVG has it as an SVG option), ppi?: number (raster formats: pixels per inch, stored in the file; wins over scale; without either, the document's raster effects resolution), scale?: number (pixels per point), background?: transparent|white|black|\"#rrggbb\" (jpg: white when transparent; #rrggbbaa and #rgba are refused here and in matte), antiAlias?: none|art (default)|type (text snapped to pixels), interlaced?: false (png, Adam7), embedIcc?: true (png: an sRGB chunk marks the colours as sRGB), quality?: 90 (jpg: 0–100), colorModel?: rgb|cmyk|gray (jpg), method?: baseline|optimized|progressive, scans?: 3 (3–5, progressive), embedIcc?: true, imageMap?: none|client|server (an HTML or NCSA map of the objects with a URL, written as .html / .map), colors?: 256 (gif/png8: 2–256), reduction?: perceptual|selective (default)|adaptive|web|blackWhite|gray, dither?: none|diffusion (default)|pattern|noise, ditherAmount?: 100, transparency?: true, matte?: white|\"#rrggbb\"|none, interlaced?, lossless?: true (webp: lossy WebP isn't available yet: written lossless, with a warning), encoding?: utf8|utf16 (txt: the stories in stacking order, back to front, a thread once; utf16 with a byte order mark), lineEndings?: lf|crlf, selectionOnly?: false, svg?: {styling, outlineText, images, objectIds, decimals, minify, responsive, useArtboards, preserveEditing, metadata, fewerTspans, hiddenLayers, encoding, profile, embedFonts, variableData} (see document.formats), …the SVG options given flat instead, …the PDF options of document.exportPdf, …the DXF options of document.exportDxf (useArtboards: one drawing per artboard), …the EPS options of document.exportEps (useArtboards: one file per artboard, {stem}_{artboard}.eps, after a central {stem}.eps of the whole document when there are two or more and art shows; else the visible art), …emf and wmf write one picture of the artboard (useArtboards: true one file per chosen artboard, false the bounds of the visible art; EMF keeps curves, clipping, transparent images and gradients as images clipped to their shape; WMF flattens curves into polygons behind a placeable header; what a format leaves out comes back in warnings), …hpgl writes a plot of the artboard as pen moves for plotters and cutters (each visible path once along its line in plotter units; useArtboards: true one file per chosen artboard, false the bounds of the visible art), hpglVersion?: hpgl (default: IN, SP, PU, PD) | hpgl2 (also NP and PC pen colors, each pen's paths as one encoded polyline PE), curveTolerance?: 0.1 (points, 0.01 to 10), pens?: [\"#rrggbb\"…] (pen 1 first, up to 255; each path takes the nearest; default a pen per color in the order the art first draws in it, numbered alike in every file), origin?: bottomLeft (default) | center, sortPaths?: false (each pen's next path the nearest), insideFirst?: false (a path after the pen's paths that lie inside it), colorModel?: rgb|cmyk|gray (tiff: rgb keeps transparency as an alpha channel), lzw?: true, byteOrder?: little|big, embedIcc?: true, colorModel?: rgb|gray (bmp), depth?: 24 (bmp: 1 black and white, 4|8 a palette by reduction and dither, 16, 24, 32 with alpha), fileFormat?: windows|os2 (1, 4, 8 or 24 bits), rle?: false (4 and 8 bits, windows), flipRows?: false (top-down rows), depth?: 24 (tga: 16 one-bit alpha, 24, 32 with alpha), colorModel?: rgb|cmyk|gray (psd), layers?: true (each top-level layer a pixel layer with its opacity and blend mode; a background colour is a Background layer; false: one flat image, on white), maxEditability?: false (layers and sublayers become groups, each object a layer named as in the Layers panel, text by its text; clipping, masked or knockout layers stay one layer), hiddenLayers?: false (hidden layers and objects written hidden instead of left out), embedIcc?: true (at most 30000 pixels a side; formats without alpha are flattened on white)} → {path, format, bytes, warnings, files?: [path…] (several), linked?: [path…] (linked images, image maps)}; no path → {dataBase64, format, bytes, warnings, files?: [{name, dataBase64}], linked?: [{name, dataBase64}]}. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is ignored. Never changes the document's path", has_doc, export::export ), diff --git a/crates/engine/src/cmd/fileio/encode.rs b/crates/engine/src/cmd/fileio/encode.rs index 31539ff51d..0fbffcea82 100644 --- a/crates/engine/src/cmd/fileio/encode.rs +++ b/crates/engine/src/cmd/fileio/encode.rs @@ -629,6 +629,13 @@ fn encode_files(doc: &Document, f: &Format, p: &Value) -> Result { { return Ok(Encoded::one_with_notes(bytes, notes)); } + // So does a `.ai` file of the art's bounds (a legacy version 7 `.ai` file has none to leave out). + if use_artboards == Some(false) + && f.id == "ai" + && let Some(enc) = super::save::encode_ai_art_page(C, f, doc, p)? + { + return Ok(enc); + } if use_artboards == Some(false) && let Some((art, q)) = art_page(C, doc, p)? { diff --git a/crates/engine/src/cmd/fileio/save.rs b/crates/engine/src/cmd/fileio/save.rs index e1cbf68bf2..777c7732a7 100644 --- a/crates/engine/src/cmd/fileio/save.rs +++ b/crates/engine/src/cmd/fileio/save.rs @@ -685,6 +685,31 @@ struct AiEditingData { editing_data: Option, } +/// Why a `.ai` file of the art's bounds (`useArtboards: false`) carries no editing data. +pub const AI_EDITING_NEEDS_THE_ARTBOARDS: &str = + "Preserve editing was left out: an .ai file of the art's bounds would replace the document's artboards when reopened"; + +/// The error for a `.ai` file of the art's bounds without PDF content. +pub const ART_PAGE_NEEDS_PDF: &str = "pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false"; + +/// With `useArtboards: false` in `p`: the PDF-compatible `.ai` file of one page, the bounds of +/// `doc`'s visible art ([`super::encode::art_page`]). It carries neither the native document nor +/// the editing copy for other apps, and its notes end with [`AI_EDITING_NEEDS_THE_ARTBOARDS`]. +/// `pdfCompatible: false` is refused. `None` when `p` asks for the artboards or for a legacy +/// version 7 `.ai` file. +pub(super) fn encode_ai_art_page(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Result> { + if super::encode::use_artboards(cmd, p)? != Some(false) || super::legacy_ai::wanted(cmd, f, p)? { + return Ok(None); + } + if !bool_or(p, "pdfCompatible", true) { + return Err(bad(cmd, ART_PAGE_NEEDS_PDF)); + } + let Some((art, q)) = super::encode::art_page(cmd, doc, p)? else { return Ok(None) }; + let mut enc = encode_ai_pages(cmd, f, &art, &q, false)?; + enc.setting_notes.push(AI_EDITING_NEEDS_THE_ARTBOARDS.to_string()); + Ok(Some(enc)) +} + /// The shared Save and Export encoder for `.ai` files (and `.ait` templates, the same file): /// PDF-compatible by default, without PDF content (`pdfCompatible: false`), or legacy version 7 /// `.ai` files (`aiVersion: "7"`). @@ -692,9 +717,15 @@ pub(super) fn encode_ai(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Res if super::legacy_ai::wanted(cmd, f, p)? { return super::legacy_ai::encode(cmd, f, doc, p); } + encode_ai_pages(cmd, f, doc, p, true) +} + +/// [`encode_ai`] of a `.ai` file with or without PDF content, carrying the native document and +/// the editing copy for other apps when `editing` is true, and neither otherwise. +fn encode_ai_pages(cmd: &str, f: &Format, doc: &Document, p: &Value, editing: bool) -> Result { let mut params = if p.is_object() { p.clone() } else { json!({}) }; if let Some(o) = params.as_object_mut() { - o.insert("preserveEditing".into(), json!(true)); + o.insert("preserveEditing".into(), json!(editing)); // Keep upstream AI PDF layers, including hidden layers, unless a standard decides. if o.get("standard").is_none_or(Value::is_null) { o.entry("createLayers").or_insert(json!(true)); diff --git a/crates/engine/src/cmd/fileio/tests_pdfedit.rs b/crates/engine/src/cmd/fileio/tests_pdfedit.rs index 2970238e38..0b560e5e04 100644 --- a/crates/engine/src/cmd/fileio/tests_pdfedit.rs +++ b/crates/engine/src/cmd/fileio/tests_pdfedit.rs @@ -402,28 +402,75 @@ fn ai_export_validates_options_even_when_editing_data_is_omitted() { } } -#[test] -fn ai_export_without_artboards_keeps_art_outside_the_original_pages() { +/// Two artboards, a visible 30 × 20 pt rectangle outside them at (180, 100) and a hidden one. +fn art_outside_the_artboards() -> Session { let mut s = Session::new(); s.execute("file.new", &json!({"width": 100, "height": 80, "artboards": 2})).unwrap(); s.execute("paint.setStroke", &json!({"none": true})).unwrap(); s.execute("shape.rectangle", &json!({"x": 180, "y": 100, "width": 30, "height": 20})).unwrap(); s.execute("shape.rectangle", &json!({"x": 1000, "y": 1000, "width": 50, "height": 40})).unwrap(); s.execute("object.hide", &json!({})).unwrap(); + s +} + +/// Whether the `.ai` file `bytes` carries the editing copy of its art for other apps. +fn carries_private_data(bytes: &[u8]) -> bool { + let opts = LoadOptions { native: false, ..Default::default() }; + load::load_with("a.ai", bytes, &opts).unwrap().editing_data +} + +#[test] +fn an_ai_of_the_arts_bounds_carries_no_editing_data() { + let mut s = art_outside_the_artboards(); let before = comparable(&s.doc().unwrap().doc); - let r = s.execute("document.export", &json!({"format": "ai", "useArtboards": false, "range": "2"})).unwrap(); - let bytes = b64(&r); - let pages = vectorcraft_pdf::import(&bytes).unwrap(); - assert_eq!(pages.artboards.len(), 1); - assert_eq!((pages.artboards[0].rect.width(), pages.artboards[0].rect.height()), (30.0, 20.0)); - let editing = vectorcraft_pdf::editing(&bytes).unwrap(); - let exported = vectorcraft_format::load(&editing.data).unwrap(); - assert_eq!(exported.artboards.len(), 1); - assert_eq!(exported.artboards[0].rect, vectorcraft_geom::Rect::new(180.0, 100.0, 210.0, 120.0)); - assert_eq!(comparable(&s.doc().unwrap().doc), before); + let left_out = |v: &Value| v["warnings"].as_array().unwrap().iter().any(|w| w == super::save::AI_EDITING_NEEDS_THE_ARTBOARDS); + let path = tmp("art-bounds.ai"); + for extra in [json!({}), json!({"range": "2"}), json!({"editingData": false}), json!({"path": path})] { + let mut params = json!({"format": "ai", "useArtboards": false}); + params.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone()); + let r = s.execute("document.export", ¶ms).unwrap(); + let bytes = if params.get("path").is_some() { std::fs::read(&path).unwrap() } else { b64(&r) }; + // One page, the visible art's bounds, whatever range names. + let pages = vectorcraft_pdf::import(&bytes).unwrap(); + assert_eq!(pages.artboards.len(), 1, "{params}"); + assert_eq!((pages.artboards[0].rect.width(), pages.artboards[0].rect.height()), (30.0, 20.0), "{params}"); + assert!(vectorcraft_pdf::editing(&bytes).is_none(), "{params}"); + assert!(!carries_private_data(&bytes), "{params}"); + assert!(left_out(&r), "{params}: {r}"); + assert_eq!(comparable(&s.doc().unwrap().doc), before); + } + // The artboards keep both editing copies, without the warning. + for params in [json!({"format": "ai"}), json!({"format": "ai", "useArtboards": true})] { + let r = s.execute("document.export", ¶ms).unwrap(); + let bytes = b64(&r); + let editing = vectorcraft_pdf::editing(&bytes).expect("editing data"); + assert_eq!(vectorcraft_format::load(&editing.data).unwrap().artboards.len(), 2, "{params}"); + assert!(carries_private_data(&bytes), "{params}"); + assert!(!left_out(&r), "{params}: {r}"); + } assert!(s.execute("document.export", &json!({"format": "ai", "useArtboards": "yes"})).is_err()); } +#[test] +fn an_ai_of_the_arts_bounds_without_pdf_is_refused() { + let mut s = art_outside_the_artboards(); + let before = comparable(&s.doc().unwrap().doc); + for extra in [json!({}), json!({"editingData": false}), json!({"editingData": true})] { + for command in ["document.export", "document.serialize"] { + let mut params = json!({"format": "ai", "useArtboards": false, "pdfCompatible": false}); + params.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone()); + let e = s.execute(command, ¶ms).unwrap_err().to_string(); + assert!(e.contains(super::save::ART_PAGE_NEEDS_PDF), "{command} {params}: {e}"); + assert_eq!(comparable(&s.doc().unwrap().doc), before); + } + } + // A document with no visible art is still refused for that. + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 80})).unwrap(); + let e = s.execute("document.export", &json!({"format": "ai", "useArtboards": false})).unwrap_err().to_string(); + assert!(e.contains("no visible art"), "{e}"); +} + /// Color Halftone's Max Radius is in pixels of the raster effects resolution since document /// version 3 (#3260). A PDF saved with Preserve Editing carries the native document, so one from /// before that opens with the radius converted, drawing the same dots; one saved since opens as diff --git a/docs/mcp.md b/docs/mcp.md index 41e28969f4..349704bfc6 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -969,7 +969,11 @@ SVG writes one file per artboard of a `range`); `useArtboards: false` exports th its effects paint, the effects of groups, type and images included. `artboard.fitToArt` measures the art the same way. `document.exportPdf {useArtboards: false}` writes that one page as well, and ignores `artboard`, `artboards` and `range`. A PDF of that page, from either command, carries no Preserve Editing data; when Preserve Editing is on (as it -is in VectorCraft Default), the export comes back with a warning that says so. +is in VectorCraft Default), the export comes back with a warning that says so. A `.ai` export with +`useArtboards: false` writes that one page too, without the native document and without the editing copy for other +apps (`editingData`), and always comes back with the warning `Preserve editing was left out: an .ai file of the art's +bounds would replace the document's artboards when reopened`; with `pdfCompatible: false` it is refused, whatever +`editingData` says. A legacy version 7 `.ai` (`aiVersion: "7"`) of the art's bounds is written as before. Export As PDF continues in the Save PDF dialog with Use Artboards on or off; with it off, the dialog has no Artboards section and the page is the bounds of the visible art. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index d9783b0216..dca049bc36 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1577,6 +1577,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur In a `.ai` opened from its editing data, Bas Relief in an art style takes its Detail, Smoothness and Light direction from the style's settings block, and an object other than type with it opens as itself with the live effect. A settings block that VectorCraft can't read (malformed, another effect's, a value out of range or a key it doesn't know) keeps the object's drawn look (part of #637; [raster effects reference](https://helpx.adobe.com/illustrator/desktop/special-effects-styles/apply-filter-effects/raster-effects-reference.html), [Photoshop File Formats Specification](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/), [scripting the filter gallery](https://community.adobe.com/questions-712/scripting-support-for-the-filter-gallery-1064646); `crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`). PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); a PDF 1.5 file with such a page is written as PDF 1.6 with PDF 1.5's encryption, PDF/X files and PDF 1.3 and 1.4 files write it in points (in PDF 1.3 and 1.4, a page past 32,767 pt that needs a real number past 32,767 is refused with an error naming it), a page whose short side is 3 pt or longer but would be under 3 units at that `UserUnit` gets a smaller one that keeps the short side at 3 units, and the export's warnings give each such page's size and how it is written (#6382). The Save PDF dialog's "Write pages longer than 200 in at full size in points" (`longPagesInPoints`, off by default) writes such pages in points without a `UserUnit` at every PDF version, in files without a PDF/X or PDF/A standard. Export As PDF with Use Artboards off also opens the Save PDF dialog, without its Artboards section, and writes one page of the visible art's bounds with the dialog's options (`document.exportPdf {useArtboards: false}`; `crates/ui-egui/src/dialogs/tests_export.rs`, `crates/engine/src/cmd/fileio/tests_pdf.rs`). + A `.ai` export with Use Artboards off writes one page of the visible art's bounds without the native document or the editing copy for other apps, with a warning, so the file does not reopen with one artboard in place of the document's; without PDF content it is refused (`crates/engine/src/cmd/fileio/tests_pdfedit.rs`). PDF export warns when a PDF/X file has no bleed on a side where the document's bleed has one, and writes the file as asked. A PDF/A-2b export at PDF 1.4 with a page longer than 14,400 pt is written as PDF 1.6 with a `UserUnit` and a warning, whether or not `longPagesInPoints` is on, so its page boundaries stay within PDF/A-2's limit of 14,400 units (ISO 19005-2, 6.1.13) unless the page is too narrow for that (`crates/pdf/src/tests/marks.rs`). PDF/X-1a, PDF/X-3 and PDF 1.3 files keep the art under the hidden part of a picture, gradient, pattern, mesh or raster effect that a clipping mask or clipping layer crops: flattening rasterizes only the part of such art that its clipping path shows, on its own page and on the next one. Layers the file leaves out are left out of the flattening, and transparent art that a clipping path hides entirely is removed (`crates/engine/src/cmd/fileio/tests_pdfx.rs`).