Merge pull request #7183 from dotCooCoo/feat/pdf-long-pages-points-default

PDF: pages longer than 200 in are written at full size in points by default
This commit is contained in:
BFlat
2026-11-14 00:49:30 +07:00
committed by GitHub
9 changed files with 117 additions and 62 deletions
File diff suppressed because one or more lines are too long
+4
View File
@@ -730,6 +730,10 @@ fn encode_ai_pages(cmd: &str, f: &Format, doc: &Document, p: &Value, editing: bo
if o.get("standard").is_none_or(Value::is_null) {
o.entry("createLayers").or_insert(json!(true));
}
// A page longer than 14,400 pt keeps its `UserUnit` (PDF export writes it in points).
if o.get("longPagesInPoints").is_none_or(Value::is_null) {
o.insert("longPagesInPoints".into(), json!(false));
}
if let Some(c) = o.get("compress").and_then(Value::as_bool) {
let compression = o.entry("compression").or_insert_with(|| json!({}));
if let Some(m) = compression.as_object_mut() {
+31 -22
View File
@@ -123,40 +123,49 @@ fn presets_export_and_import_round_trip() {
assert!(is_bad_params(Session::new().execute("pdf.preset.export", &json!({}))), "nothing saved to export");
}
/// `longPagesInPoints` is saved with a preset, comes back through a presets file and the
/// preferences, shows in `document.pdfSettings`, and a 10 m page exported with it reopens at 10 m.
/// A value that isn't a boolean is refused.
/// `longPagesInPoints` is on by default: a 10 m page exported without options is written in
/// points and reopens at 10 m. Turned off, it is saved with a preset, comes back through a presets
/// file and the preferences, shows in `document.pdfSettings`, and the page exported with it has a
/// `UserUnit` and reopens at 10 m. A value that isn't a boolean is refused.
#[test]
fn long_pages_in_points_round_trips_through_presets_and_reopens_at_full_size() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 28_346.456_692_913_386, "height": 2_834.645_669_291_338_5})).unwrap();
s.execute("pdf.preset.save", &json!({"name": "Banner", "longPagesInPoints": true, "preserveEditing": false})).unwrap();
assert!(s.prefs.pdf_presets[0].settings.long_pages_in_points);
let reopened = |bytes: &[u8]| {
let mut o = Session::new();
o.execute("document.open", &json!({"name": "banner.pdf", "dataBase64": vectorcraft_format::base64_encode(bytes)})).unwrap();
let rect = o.doc().unwrap().doc.artboards[0].rect;
assert!((rect.width() - 28_346.46).abs() < 0.01 && (rect.height() - 2_834.65).abs() < 0.01, "{rect:?}");
};
let warned = |v: &Value, end: &str| v["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().ends_with(end));
// By default the page is written in points.
assert_eq!(s.execute("document.pdfSettings", &json!({})).unwrap()["settings"]["longPagesInPoints"], json!(true));
let v = s.execute("document.exportPdf", &json!({"preserveEditing": false})).unwrap();
let bytes = pdf(&v);
assert!(!String::from_utf8_lossy(&bytes).contains("/UserUnit"));
assert!(warned(&v, "written without a UserUnit, in points, and apps that keep to the limit cut the page to 200 in"), "{v}");
reopened(&bytes);
// Off in a preset.
s.execute("pdf.preset.save", &json!({"name": "Banner", "longPagesInPoints": false, "preserveEditing": false})).unwrap();
assert!(!s.prefs.pdf_presets[0].settings.long_pages_in_points);
let back: crate::Prefs = serde_json::from_value(s.prefs.to_json()).unwrap();
assert_eq!(back.pdf_presets, s.prefs.pdf_presets);
let text = s.execute("pdf.preset.export", &json!({"names": ["Banner"]})).unwrap()["data"].as_str().unwrap().to_string();
let mut t = session();
t.execute("pdf.preset.import", &json!({"data": text})).unwrap();
assert_eq!(t.prefs.pdf_presets, s.prefs.pdf_presets);
let v = s.execute("document.pdfSettings", &json!({"longPagesInPoints": true})).unwrap();
assert_eq!((&v["settings"]["longPagesInPoints"], &v["changed"]), (&json!(true), &json!([{"option": "longPagesInPoints", "value": true}])), "{v}");
assert_eq!(s.execute("document.pdfSettings", &json!({})).unwrap()["settings"]["longPagesInPoints"], json!(false));
// The preset writes the page in points; the file reopens at 10 m.
let v = s.execute("document.exportPdf", &json!({"preset": "Banner"})).unwrap();
let bytes = pdf(&v);
assert!(!String::from_utf8_lossy(&bytes).contains("/UserUnit"));
assert!(
v["warnings"]
.as_array()
.unwrap()
.iter()
.any(|w| w.as_str().unwrap().ends_with("written without a UserUnit, in points, and apps that keep to the limit cut the page to 200 in")),
let v = s.execute("document.pdfSettings", &json!({"longPagesInPoints": false})).unwrap();
assert_eq!(
(&v["settings"]["longPagesInPoints"], &v["changed"]),
(&json!(false), &json!([{"option": "longPagesInPoints", "value": false}])),
"{v}"
);
let mut o = Session::new();
o.execute("document.open", &json!({"name": "banner.pdf", "dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap();
let rect = o.doc().unwrap().doc.artboards[0].rect;
assert!((rect.width() - 28_346.46).abs() < 0.01 && (rect.height() - 2_834.65).abs() < 0.01, "{rect:?}");
// The preset writes the page with a `UserUnit`; the file reopens at 10 m.
let v = s.execute("document.exportPdf", &json!({"preset": "Banner"})).unwrap();
let bytes = pdf(&v);
assert!(String::from_utf8_lossy(&bytes).contains("/UserUnit 1.969"));
assert!(warned(&v, "it is written with a UserUnit of 1.969, so apps that ignore UserUnit show it that many times smaller"), "{v}");
reopened(&bytes);
for bad in [json!("yes"), json!(1)] {
assert!(is_bad_params(s.execute("document.exportPdf", &json!({"longPagesInPoints": bad}))), "{bad}");
assert!(is_bad_params(s.execute("pdf.preset.save", &json!({"name": "Bad", "longPagesInPoints": bad}))), "{bad}");
+27 -4
View File
@@ -217,7 +217,7 @@ choice! {
}
/// Save PDF settings (the dialog's sections; what a preset stores).
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PdfSettings {
pub standard: Standard,
@@ -237,9 +237,9 @@ pub struct PdfSettings {
/// `create_layers` is on).
pub include_non_printing: bool,
/// Write a page with a side longer than 14,400 pt (200 in) at its size in points, without a
/// `UserUnit`, at every PDF version, in files without a standard (off, and in PDF/A files:
/// PDF 1.6 and later files that aren't PDF/X files write it with a `UserUnit`, and PDF 1.5
/// files are written as PDF 1.6 for it).
/// `UserUnit`, at every PDF version, in files without a standard (on by default; off, and in
/// PDF/A files: PDF 1.6 and later files that aren't PDF/X files write it with a `UserUnit`,
/// and PDF 1.5 files are written as PDF 1.6 for it).
pub long_pages_in_points: bool,
pub compression: CompressionSettings,
pub marks: MarkSettings,
@@ -253,6 +253,29 @@ pub struct PdfSettings {
pub flattener_preset: String,
}
impl Default for PdfSettings {
fn default() -> Self {
Self {
standard: Standard::default(),
compatibility: Compatibility::default(),
preserve_editing: false,
thumbnails: false,
fast_web_view: false,
view_after_saving: false,
create_layers: false,
include_non_printing: false,
long_pages_in_points: true,
compression: CompressionSettings::default(),
marks: MarkSettings::default(),
bleed: BleedSettings::default(),
output: OutputSettings::default(),
advanced: AdvancedSettings::default(),
security: SecuritySettings::default(),
flattener_preset: String::new(),
}
}
}
/// Resampling and compression of one kind of image.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
+32 -25
View File
@@ -105,8 +105,9 @@ fn units_and_media(bytes: &[u8]) -> Vec<(f32, Rect)> {
}
/// A page with a side longer than ISO 32000-1's 14,400 units is written in larger units (its
/// `UserUnit`), so that its sides are 14,400 units at most, and reads back at its size in points.
/// Pages up to 14,400 pt, files before PDF 1.6 and PDF/X files keep their sizes in points.
/// `UserUnit`), so that its sides are 14,400 units at most, and reads back at its size in points,
/// with `longPagesInPoints` off. Pages up to 14,400 pt, files before PDF 1.6 and PDF/X files keep
/// their sizes in points.
#[test]
fn a_page_past_14400_pt_is_written_with_a_user_unit_and_reads_back_at_its_size() {
let mut d = doc();
@@ -117,7 +118,7 @@ fn a_page_past_14400_pt_is_written_with_a_user_unit_and_reads_back_at_its_size()
small.rect = Rect::new(0.0, 12_000.0, 20_000.0, 12_001.0);
d.artboards.push(small);
let near = |a: Rect, b: Rect| [a.x0 - b.x0, a.y0 - b.y0, a.x1 - b.x1, a.y1 - b.y1].iter().all(|d| d.abs() < 0.01);
let bytes = pdf(&d, json!({}));
let bytes = pdf(&d, json!({"longPagesInPoints": false}));
let pages = units_and_media(&bytes);
// 20,000 / 14,400 = 1.3889, rounded up to 1.389: the page is 14,398.85 × 7,199.42 units.
assert_eq!(pages[0].0, 1.389, "{pages:?}");
@@ -155,6 +156,12 @@ fn report(d: &Document, set: PdfSettings) -> Result<ExportReport, PdfError> {
export_with_report(d, &PdfOptions { settings: set, created: Some(1_791_200_000), ..PdfOptions::uncompressed() })
}
/// The default settings with `long_pages_in_points` off: long pages from PDF 1.6 on get a
/// `UserUnit`.
fn user_units() -> PdfSettings {
PdfSettings { long_pages_in_points: false, ..PdfSettings::default() }
}
/// The numbers that follow `key` in the file's bytes (`/MediaBox [0 0 w h]`, `/UserUnit u`), read
/// as text, without the PDF reader: none when the file has no `key`.
fn raw_numbers(bytes: &[u8], key: &str, count: usize) -> Vec<f64> {
@@ -178,13 +185,13 @@ fn long_page_warnings(r: &ExportReport) -> Vec<&String> {
r.warnings.iter().filter(|w| w.contains("page limit of 14,400 pt")).collect()
}
/// A 20 m page at PDF 1.7 (the default) is written with a `UserUnit` of 3.938, reads back at 20 m
/// from the file's bytes, through the PDF reader and through the import, and the export says how
/// it is written.
/// A 20 m page at PDF 1.7 (the default) with `long_pages_in_points` off is written with a
/// `UserUnit` of 3.938, reads back at 20 m from the file's bytes, through the PDF reader and
/// through the import, and the export says how it is written.
#[test]
fn a_20_m_page_keeps_its_size_with_a_user_unit_and_the_export_says_so() {
let d = sized(TWENTY_M, TWENTY_M / 20.0);
let r = report(&d, PdfSettings::default()).unwrap();
let r = report(&d, user_units()).unwrap();
assert!(r.bytes.starts_with(b"%PDF-1.7"));
assert_eq!(
long_page_warnings(&r),
@@ -209,7 +216,7 @@ fn a_20_m_page_keeps_its_size_with_a_user_unit_and_the_export_says_so() {
/// Every built-in preset and every PDF version keeps a 20 m page at its size, or refuses it with
/// the reason: PDF 1.3 and 1.4 files (PDF/X-1a and PDF/X-3 among them) have no `UserUnit` and
/// hold no real number past 32,767, PDF 1.5 is written as PDF 1.6 to have one, and PDF/X-4 pages
/// at PDF 1.5 or 1.6 stay in points with a warning.
/// at PDF 1.5 or 1.6 stay in points with a warning (with `long_pages_in_points` off).
#[test]
fn every_preset_and_version_keeps_a_20_m_page_or_says_why_not() {
let d = sized(TWENTY_M, TWENTY_M / 20.0);
@@ -223,7 +230,7 @@ fn every_preset_and_version_keeps_a_20_m_page_or_says_why_not() {
};
for preset in crate::builtin_presets() {
let name = preset.name.as_str();
match report(&d, preset.settings.clone()) {
match report(&d, PdfSettings { long_pages_in_points: false, ..preset.settings.clone() }) {
Err(e) if matches!(name, "PDF/X-1a:2001" | "PDF/X-3:2002") => refused(e, name),
Err(e) => panic!("{name}: {e}"),
Ok(r) if name == "PDF/X-4:2010" => {
@@ -241,7 +248,7 @@ fn every_preset_and_version_keeps_a_20_m_page_or_says_why_not() {
}
}
for c in Compatibility::ALL.iter().copied() {
let set = PdfSettings { compatibility: c, ..PdfSettings::default() };
let set = PdfSettings { compatibility: c, ..user_units() };
match (c, report(&d, set)) {
(Compatibility::Pdf13 | Compatibility::Pdf14, Err(e)) => refused(e, c.label()),
(Compatibility::Pdf13 | Compatibility::Pdf14, Ok(_)) => panic!("{} is refused", c.label()),
@@ -362,7 +369,7 @@ fn the_error_names_the_long_page_whose_art_needs_a_larger_real() {
#[test]
fn a_page_too_narrow_for_its_user_unit_keeps_3_units_on_its_short_side() {
const MM: f64 = 72.0 / 25.4;
for set in [PdfSettings::default(), PdfSettings { standard: Standard::PdfA2b, ..PdfSettings::default() }] {
for set in [user_units(), PdfSettings { standard: Standard::PdfA2b, ..PdfSettings::default() }] {
let r = report(&sized(TWENTY_M * 5.0, 4.0 * MM), set).unwrap();
assert_eq!(raw_numbers(&r.bytes, "/UserUnit", 1), [3.779]);
let media = raw_numbers(&r.bytes, "/MediaBox", 4);
@@ -377,11 +384,11 @@ fn a_page_too_narrow_for_its_user_unit_keeps_3_units_on_its_short_side() {
assert!(long_page_warnings(&r).is_empty(), "{:?}", r.warnings);
}
// A short side of 3 units or more at the long side's unit keeps that unit.
let r = report(&sized(TWENTY_M * 5.0, 60.0), PdfSettings::default()).unwrap();
let r = report(&sized(TWENTY_M * 5.0, 60.0), user_units()).unwrap();
assert_eq!(raw_numbers(&r.bytes, "/UserUnit", 1), [19.686]);
let media = raw_numbers(&r.bytes, "/MediaBox", 4);
assert!(media[3] - media[1] >= 3.0, "{media:?}");
let r = report(&sized(TWENTY_M * 5.0, 2.0), PdfSettings::default()).unwrap();
let r = report(&sized(TWENTY_M * 5.0, 2.0), user_units()).unwrap();
assert_eq!(raw_numbers(&r.bytes, "/UserUnit", 1), [19.686]);
}
@@ -389,7 +396,7 @@ fn a_page_too_narrow_for_its_user_unit_keeps_3_units_on_its_short_side() {
/// the Save PDF dialog shows it.
#[test]
fn a_pdf_1_5_file_written_as_1_6_keeps_rc4_encryption() {
let mut set = PdfSettings { compatibility: Compatibility::Pdf15, ..PdfSettings::default() };
let mut set = PdfSettings { compatibility: Compatibility::Pdf15, ..user_units() };
set.security.open_password = "secret".into();
let has = |bytes: &[u8], s: &[u8]| bytes.windows(s.len()).any(|w| w == s);
let r = report(&sized(TWENTY_M, 300.0), set).unwrap();
@@ -408,7 +415,7 @@ fn the_long_page_warning_rounds_the_size_up() {
/// point longer gets a `UserUnit` of 1.001 and the warning, and stays in points at PDF 1.4.
#[test]
fn a_page_of_14400_pt_stays_in_points_and_one_point_more_does_not() {
let at = |side: f64, c: Compatibility| report(&sized(side, 300.0), PdfSettings { compatibility: c, ..PdfSettings::default() });
let at = |side: f64, c: Compatibility| report(&sized(side, 300.0), PdfSettings { compatibility: c, ..user_units() });
for c in [Compatibility::Pdf14, Compatibility::Pdf15, Compatibility::Pdf17] {
let r = at(14_400.0, c).unwrap();
assert!(long_page_warnings(&r).is_empty() && !r.warnings.iter().any(|w| w.contains("UserUnit")), "{}: {:?}", c.label(), r.warnings);
@@ -455,13 +462,14 @@ fn a_page_within_14400_pt_is_written_as_before_at_every_version() {
/// 10 m in points.
const TEN_M: f64 = 10.0 / 0.0254 * 72.0;
/// With `long_pages_in_points`, a 10 m page at PDF 1.7 is written at its size in points, with no
/// `UserUnit`, the warning says so, and the page reads back at 10 m. Without it the page keeps
/// its `UserUnit`.
/// With `long_pages_in_points` (on by default), a 10 m page at PDF 1.7 is written at its size in
/// points, with no `UserUnit`, the warning says so, and the page reads back at 10 m. Without it the
/// page keeps its `UserUnit`.
#[test]
fn long_pages_in_points_writes_a_10_m_page_in_points() {
let d = sized(TEN_M, TEN_M / 10.0);
let r = report(&d, PdfSettings { long_pages_in_points: true, ..PdfSettings::default() }).unwrap();
assert!(settings(json!({})).long_pages_in_points, "on by default");
let r = report(&d, PdfSettings::default()).unwrap();
assert!(r.bytes.starts_with(b"%PDF-1.7"));
assert_eq!(raw_numbers(&r.bytes, "/UserUnit", 1), Vec::<f64>::new());
let media = raw_numbers(&r.bytes, "/MediaBox", 4);
@@ -479,13 +487,12 @@ fn long_pages_in_points_writes_a_10_m_page_in_points() {
let back = import(&r.bytes).unwrap();
let rect = back.artboards[0].rect;
assert!((rect.width() - TEN_M).abs() < 0.01 && (rect.height() - TEN_M / 10.0).abs() < 0.01, "{rect:?}");
// Off (the default), the page has a `UserUnit` of 28,346.46 / 14,400, rounded up.
let off = report(&d, PdfSettings::default()).unwrap();
// Off, the page has a `UserUnit` of 28,346.46 / 14,400, rounded up.
let off = report(&d, user_units()).unwrap();
assert_eq!(raw_numbers(&off.bytes, "/UserUnit", 1), [1.969]);
assert!(
long_page_warnings(&off)[0].ends_with("it is written with a UserUnit of 1.969, so apps that ignore UserUnit show it that many times smaller")
);
assert!(!settings(json!({})).long_pages_in_points, "off by default");
}
/// With `long_pages_in_points`, every PDF version writes a 10 m page in points: PDF 1.5 stays
@@ -496,7 +503,7 @@ fn long_pages_in_points_keeps_every_version_and_changes_nothing_where_pages_are_
let d = sized(TEN_M, TEN_M / 10.0);
let with = |set: &PdfSettings| PdfSettings { long_pages_in_points: true, ..set.clone() };
for c in Compatibility::ALL.iter().copied() {
let set = PdfSettings { compatibility: c, ..PdfSettings::default() };
let set = PdfSettings { compatibility: c, ..user_units() };
let r = report(&d, with(&set)).unwrap();
assert!(r.bytes.starts_with(format!("%PDF-{}", c.id()).as_bytes()), "{}", c.label());
assert_eq!(raw_numbers(&r.bytes, "/UserUnit", 1), Vec::<f64>::new(), "{}", c.label());
@@ -520,7 +527,7 @@ fn pdfa2b_long_page_stays_within_14400_units() {
let d = sized(TEN_M, TEN_M / 10.0);
for s in Standard::ALL.iter().copied().filter(|s| *s != Standard::None && !s.is_pdfx()) {
for c in Compatibility::ALL.iter().copied().filter(|c| s.allows(*c)) {
let off = PdfSettings { standard: s, compatibility: c, ..PdfSettings::default() };
let off = PdfSettings { standard: s, compatibility: c, ..user_units() };
let on = PdfSettings { long_pages_in_points: true, ..off.clone() };
let (on, off) = (report(&d, on).unwrap(), report(&d, off).unwrap());
assert_eq!(on.bytes, off.bytes, "{} {}", s.label(), c.label());
@@ -540,7 +547,7 @@ fn pdfa2b_long_page_stays_within_14400_units() {
fn long_pages_in_points_leaves_a_page_within_14400_pt_as_it_is() {
let d = sized(14_400.0, 300.0);
for c in Compatibility::ALL.iter().copied() {
let off = PdfSettings { compatibility: c, ..PdfSettings::default() };
let off = PdfSettings { compatibility: c, ..user_units() };
let on = PdfSettings { long_pages_in_points: true, ..off.clone() };
let (off, on) = (report(&d, off).unwrap(), report(&d, on).unwrap());
assert_eq!(on.bytes, off.bytes, "{}", c.label());
+2 -2
View File
@@ -251,14 +251,14 @@ fn long_page_notes_are_translated() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 56_692.91, "height": 2_834.65})).unwrap();
let mut notes: Vec<String> = vec![];
for p in [json!({"compatibility": "1.5"}), json!({"preset": "PDF/X-4:2010"})] {
for p in [json!({"compatibility": "1.5", "longPagesInPoints": false}), json!({"preset": "PDF/X-4:2010"})] {
let r = s.execute("document.exportPdf", &p).unwrap();
let warnings = r["warnings"].as_array().unwrap().iter().filter_map(|w| w.as_str());
notes.extend(warnings.filter(|w| w.contains("14,400 pt")).map(str::to_string));
}
notes.push(s.execute("document.exportPdf", &json!({"preset": "PDF/X-1a:2001"})).unwrap_err().to_string());
s.execute("file.new", &json!({"width": 85_040, "height": 17})).unwrap();
let r = s.execute("document.exportPdf", &json!({})).unwrap();
let r = s.execute("document.exportPdf", &json!({"longPagesInPoints": false})).unwrap();
let warnings = r["warnings"].as_array().unwrap().iter().filter_map(|w| w.as_str());
notes.extend(warnings.filter(|w| w.contains("too long and narrow")).map(str::to_string));
assert_eq!(notes.len(), 5, "{notes:?}");
+12 -2
View File
@@ -205,7 +205,8 @@ fn svg_continues_in_svg_options() {
/// Without Use Artboards, a PDF continues in Save PDF too, without its artboard choice, and the
/// file is one page of the art's bounds. Its options apply: a drawing 10 m long is written at
/// full size in points with "Write pages longer than 200 in at full size in points".
/// full size in points with "Write pages longer than 200 in at full size in points" (on by
/// default), and with a `UserUnit` without it.
#[test]
fn pdf_art_continues_in_the_save_pdf_dialog() {
let (mut app, written) = app(2);
@@ -223,13 +224,22 @@ fn pdf_art_continues_in_the_save_pdf_dialog() {
app.run("shape.rectangle", json!({"x": 0, "y": 100, "width": 28_346.46, "height": 50})).unwrap();
app.run("file.exportAs", json!({"format": "pdf"})).unwrap();
confirm(&mut app).unwrap();
set(&mut app, "longPagesInPoints", json!(true));
frame(&mut app);
confirm(&mut app).unwrap();
let bytes = written.borrow()[1].1.clone();
assert!(!String::from_utf8_lossy(&bytes).contains("/UserUnit"));
let width = vectorcraft_pdf::import(&bytes).unwrap().artboards[0].rect.width();
assert!((28_346.0..28_350.0).contains(&width), "{width}");
app.run("file.exportAs", json!({"format": "pdf"})).unwrap();
confirm(&mut app).unwrap();
set(&mut app, "longPagesInPoints", json!(false));
frame(&mut app);
confirm(&mut app).unwrap();
let bytes = written.borrow()[2].1.clone();
assert!(String::from_utf8_lossy(&bytes).contains("/UserUnit"));
let width = vectorcraft_pdf::import(&bytes).unwrap().artboards[0].rect.width();
assert!((28_346.0..28_350.0).contains(&width), "{width}");
}
#[test]
+5 -3
View File
@@ -1040,7 +1040,8 @@ and `flattener` options over it (as `object.flattenTransparency` takes them); im
transparency and rasterized areas are clipped to their regions. Layers the file leaves out (non-printing ones, unless
`includeNonPrinting` or `createLayers` is set) are left out of the flattening. A file that would still have transparency fails the
export. PDF/X-1a and PDF/X-3 files are PDF 1.3 files too.
A page with a side longer than 14,400 pt is written with a `UserUnit` (PDF 1.6): its longer side divided by 14,400,
In PDF/A-2b files, `.ai` saves and files written with `longPagesInPoints: false`, a page with a side longer than
14,400 pt is written with a `UserUnit` (PDF 1.6): its longer side divided by 14,400,
rounded up to the next thousandth, with the page's sizes and coordinates in those units, 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 a warning, and
keeps PDF 1.5's encryption (128-bit RC4). A PDF/A-2b file at PDF 1.4 with such a page is written as PDF 1.6 too, with a
@@ -1049,9 +1050,10 @@ unless the page is too narrow for that (below)
([veraPDF rule 6.1.13-11](https://github.com/veraPDF/veraPDF-validation-profiles/wiki/PDFA-Parts-2-and-3-rules#rule-6113-11);
PDF/A-2 files may declare PDF 1.0 to 1.7, [rule 6.1.2-1](https://github.com/veraPDF/veraPDF-validation-profiles/wiki/PDFA-Parts-2-and-3-rules#rule-612-1)).
PDF/X files and other PDF 1.3 and 1.4 files write such a page in points, and apps
that keep to the limit cut it to 200 in. `longPagesInPoints: true` (off by default; "Write pages longer than 200 in at
that keep to the limit cut it to 200 in. `longPagesInPoints` (on by default; "Write pages longer than 200 in at
full size in points" in the Save PDF dialog's General section) writes such a page in points at every PDF version, without
a `UserUnit`, and a PDF 1.5 file stays PDF 1.5; the warning then says the page is written in points. The option applies
a `UserUnit`, and a PDF 1.5 file stays PDF 1.5; the warning then says the page is written in points. Readers that
ignore `UserUnit` show such a page at its full size. The option applies
only to files without a standard: PDF/X files write such a page in points anyway, and PDF/A files keep the `UserUnit`, which
keeps their page boundaries within PDF/A-2's limit of 14,400 units (ISO 19005-2, 6.1.13), a PDF/A file at PDF 1.4
included (written as PDF 1.6). The dialog disables the
+1 -1
View File
@@ -1604,7 +1604,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, an envelope made with a warp, a mesh or a top object opens as a live envelope of its contents, with its warp style, bend and distortions, its mesh points and handles or its top object's outline, its Envelope Options, its Fidelity and whether its contents are being edited. When Distort Appearance is on and the size the file saves for the contents matches their bounds with strokes, the contents map from that stroked box, until an edit in Edit Contents changes how far the strokes reach; VectorCraft's new envelopes map from the geometric box, as do contents with no width or no height, such as a single straight line. An envelope that can't be read, or that VectorCraft draws differently from the file's drawn art, keeps that drawn art, with a note. An envelope of type opens live, its type laid out from the text document as its contents, where VectorCraft's own envelope of that type draws as the drawn art the file saves (compared as for the other envelopes, seen on white and on black, so white type counts, and only where the layers with it are compared with the page and draw as it, the envelope weighed as the art it draws); elsewhere, and where the type's font isn't available, a text or visibility variable binds the type, or the envelope is in a symbol's or a pattern's art, it keeps its drawn art, with its type hidden and a note that says the type isn't editable (#3179; `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`). A warp of type whose own warp, VectorCraft's, doesn't draw as its drawn art is tried again as a mesh envelope on the grid the file saves for it: its type stays editable and keeps the file's shape, but the warp's settings (style, bend) are lost, as the document has no place for them on a mesh. Type with strokes in an envelope with Distort Appearance on is also tried mapped from its glyphs grown by as far as its strokes paint along a side (half the weight of a centred stroke), without what miter joins add at corners, where the size the file saves for it doesn't already give that box; after those tries (and before a grid moved onto its drawn art), it is also tried mapped from the box of what it paints, where that is another box, as users' files map some such type: its glyph outlines with their fills and strokes baked as the envelope bakes them, so geometry effects on them count (a stroke a Transform effect moves paints where it is moved to), each measured by its visual bounds (exact for miter joins, a tip counting where the miter limit keeps it, and for round joins; an upper bound for bevel joins, miters the limit cuts, width profiles and square-capped dashes), and a contour of zero length, which paints nothing, left out; effects on the type object itself are left out, as VectorCraft applies them after the envelope bends the type. The drawn-art check decides which one opens. An envelope's grid reads its way round from all its rows and columns, so a mesh bent far round reads the way it runs. While an envelope's contents are edited, its mesh is kept in its dictionary, and in some files that mesh lies away from the drawn art by a move, the contents with the drawn art (as if the mesh hadn't followed the envelope when it moved): a mesh or top object saved in Edit Contents whose mesh draws the contents as large as the drawn art but elsewhere is tried again, after every other way, with its mesh moved onto that art, and opens there where it then draws as it. The drawn art of these files strokes type in an envelope along its bent outlines at the stroke's own weight, Distort Appearance on or off, and VectorCraft draws it so (#6644): an envelope of stroked type whose grid squeezes or stretches it opens live where it draws as that art.
In a `.ai` opened from its editing data, Convert to Shape on a whole path or type object opens as a live Rectangle, Rounded Rectangle or Ellipse effect with its Absolute or Relative size, its corner radius, its place among the object's effects and whether it is hidden. A relative size in the file is added to each side. A relative Ellipse, or a setting VectorCraft doesn't read, keeps the drawn art (`crates/eps/src/import/ai/style/tests.rs`, `crates/eps/src/import/ai/tests.rs`, `crates/eps/src/import/tests_native.rs`).
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.
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`, on by default; `.ai` saves leave it off) writes such pages in points without a `UserUnit` at every PDF version, in files without a PDF/X or PDF/A standard, so readers that ignore `UserUnit` show them at full size.
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.