Merge #603: Part of #372: .ai files write layers and sublayers as PDF layers, hidden ones kept

# Conflicts:
#	ROADMAP.md
This commit is contained in:
bflatastic
2026-10-09 04:46:09 +07:00
19 changed files with 223 additions and 46 deletions
File diff suppressed because one or more lines are too long
+7 -2
View File
@@ -306,11 +306,16 @@ fn blank_pages(doc: &Document) -> Document {
const NOT_PDF_COMPATIBLE: &str = "saved without PDF content: VectorCraft opens it as before, other apps show empty pages";
/// The encoder's params for a `.ai` file, from a save's or an export's: a PDF of every artboard
/// with the PDF options, always carrying the native document; Use Compression compresses the
/// PDF's content.
/// with the PDF options, always carrying the native document, its layers and sublayers PDF layers
/// (hidden ones off) unless asked otherwise; Use Compression compresses the PDF's content.
pub(super) fn ai_params(params: &mut Value) {
let Some(o) = params.as_object_mut() else { return };
o.extend([("preserveEditing".into(), json!(true)), ("range".into(), json!("all"))]);
// Apps that read the PDF part (not the native document) then find the layers, the hidden ones
// with their art (#372); a PDF standard decides for itself.
if o.get("standard").is_none_or(Value::is_null) {
o.entry("createLayers").or_insert(json!(true));
}
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() {
@@ -59,3 +59,35 @@ fn export_pdf_overprint_is_preserved_or_discarded() {
assert_eq!(overprints(&mut s, json!({"compression": {"compressText": false}})), 1);
assert_eq!(overprints(&mut s, json!({"advanced": {"overprint": "discard"}})), 0);
}
/// A `.ai` file's PDF part, which apps that don't read the native document open, has the layers
/// and sublayers as PDF layers, the hidden ones with their art (#372).
#[test]
fn ai_files_write_their_layers_and_sublayers_as_pdf_layers() {
let mut s = session();
let layer1 = s.execute("document.inspect", &json!({})).unwrap()["layers"][1]["id"].as_u64().unwrap();
let sub = s.execute("layer.newSublayer", &json!({"parent": layer1, "name": "Sketch"})).unwrap()["id"].as_u64().unwrap();
s.execute("layer.setCurrent", &json!({"id": sub})).unwrap();
s.execute("shape.ellipse", &json!({"x": 10, "y": 40, "width": 20, "height": 20})).unwrap();
s.execute("layer.setProps", &json!({"id": sub, "visible": false})).unwrap();
// Name, shown and what it holds (sublayers by name), top-level layers in paint order.
fn rows(layers: &[std::sync::Arc<vectorcraft_doc::Node>]) -> Vec<(String, bool, Vec<String>)> {
layers
.iter()
.map(|l| {
let held =
l.children().into_iter().flatten().map(|c| c.name.clone().filter(|_| c.is_layer()).unwrap_or_else(|| "art".into())).collect();
(l.name.clone().unwrap_or_default(), l.visible, held)
})
.collect()
}
let v = s.execute("document.export", &json!({"format": "ai"})).unwrap();
let back = vectorcraft_pdf::import(&b64(&v)).unwrap();
let art = || "art".to_string();
assert_eq!(rows(&back.layers), [("Layer 1".to_string(), true, vec![art(), "Sketch".into()]), ("Notes".to_string(), false, vec![art()])]);
let sketch = back.layers.first().and_then(|l| l.children()?.last().cloned()).unwrap();
assert_eq!(rows(&[sketch]), [("Sketch".to_string(), false, vec![art()])]);
// Asked not to, it writes none.
let v = s.execute("document.export", &json!({"format": "ai", "createLayers": false})).unwrap();
assert_eq!(vectorcraft_pdf::import(&b64(&v)).unwrap().layers.len(), 1);
}