diff --git a/crates/engine/src/inspect.rs b/crates/engine/src/inspect.rs index bdda8c78f8..22c3d3a96f 100644 --- a/crates/engine/src/inspect.rs +++ b/crates/engine/src/inspect.rs @@ -38,7 +38,7 @@ pub fn document(d: &DocState) -> Value { "isolatedLayers": d.isolated_layers.iter().map(|l| l.0).collect::>(), "hasSelection": doc.selection.is_some(), "selectionBounds": doc.selection.as_ref().map(|s| { let r = s.content_bounds(); [r.x0, r.y0, r.width() as i32, r.height() as i32] }), - "layers": doc.layers.iter().rev().map(|l| layer_sel(l, &selected)).collect::>(), + "layers": doc.layers.iter().rev().map(|l| layer_sel(l, &selected, Some(&doc.metadata))).collect::>(), "history": d.history.entries(), "snapshots": d.snapshots.iter().map(|snap| json!({"id": snap.id, "name": snap.name})).collect::>(), "canUndo": d.history.can_undo(), @@ -63,11 +63,12 @@ pub fn document(d: &DocState) -> Value { /// Layer tree top-to-bottom (display order). pub fn layer(l: &Layer) -> Value { - layer_sel(l, &[]) + layer_sel(l, &[], None) } -/// [`layer`] with a `selected` flag on every node (Layers panel multi-selection). -fn layer_sel(l: &Layer, selected: &[photocraft_doc::LayerId]) -> Value { +/// [`layer`] with a `selected` flag on every node (Layers panel multi-selection); with the +/// document's `meta`, a smart object whose file is embedded in the PSD says so. +fn layer_sel(l: &Layer, selected: &[photocraft_doc::LayerId], meta: Option<&photocraft_doc::Metadata>) -> Value { let bounds = l.surface().map(|s| { let r = s.content_bounds(); [r.x0, r.y0, r.width() as i32, r.height() as i32] @@ -91,7 +92,7 @@ fn layer_sel(l: &Layer, selected: &[photocraft_doc::LayerId]) -> Value { }); match &l.content { LayerContent::Group(g) => { - v["children"] = Value::Array(g.children.iter().rev().map(|c| layer_sel(c, selected)).collect()); + v["children"] = Value::Array(g.children.iter().rev().map(|c| layer_sel(c, selected, meta)).collect()); v["expanded"] = json!(g.expanded); if let Some(a) = &g.artboard { v["artboard"] = json!({"rect": [a.rect.x0, a.rect.y0, a.rect.width(), a.rect.height()], "background": a.background.name(), "preset": a.preset}); @@ -111,11 +112,15 @@ fn layer_sel(l: &Layer, selected: &[photocraft_doc::LayerId]) -> Value { ); v["smartFiltersEnabled"] = json!(sm.filters_enabled); // Where the contents come from. A PSD placed layer (embedded or linked in Photoshop) is - // `linked` to the `Idnt` uuid of its file, which its duplicates share; New Smart Object - // via Copy embeds its own copy of the file instead. + // `linked` to the `Idnt` uuid of its file, which its duplicates share; when the file + // is embedded in the PSD (a `liFD` item) it is reported as embedded, with that uuid. + // New Smart Object via Copy embeds its own copy of the file instead. v["smartSource"] = match &sm.source { SmartSource::Embedded { file_name, .. } => json!({"kind": "embedded", "fileName": file_name}), - SmartSource::Linked { path } => json!({"kind": "linked", "path": path}), + SmartSource::Linked { path } => match meta.and_then(|m| photocraft_io::linked::embedded_file_name(m, path)) { + Some(name) => json!({"kind": "embedded", "fileName": name, "uuid": path}), + None => json!({"kind": "linked", "path": path}), + }, }; } LayerContent::Text(t) => { diff --git a/crates/engine/src/smart_cmds/tests.rs b/crates/engine/src/smart_cmds/tests.rs index 259ae92c43..a764712c9f 100644 --- a/crates/engine/src/smart_cmds/tests.rs +++ b/crates/engine/src/smart_cmds/tests.rs @@ -1062,6 +1062,10 @@ fn inspect_reports_the_smart_source() { let source = |s: &Session| crate::inspect::layer(s.active().unwrap().doc.layer(s.active().unwrap().active_layer.unwrap()).unwrap())["smartSource"].clone(); let front = source(&s); assert_eq!(front, json!({"kind": "linked", "path": "uuid-1"})); + // The document's info knows the file is embedded in the PSD (a `liFD` item) and says so, + // with its file name; the uuid stays its key. + let info = crate::inspect::document(s.active().unwrap()); + assert_eq!(info["layers"][0]["smartSource"], json!({"kind": "embedded", "fileName": "in.png", "uuid": "uuid-1"})); // A duplicate shares the file; New Smart Object via Copy gets its own. s.execute("layer.duplicate", json!({})).unwrap(); assert_eq!(source(&s), front, "a duplicate shares its source"); diff --git a/crates/io/src/linked.rs b/crates/io/src/linked.rs index eaf77e864f..6634735ef0 100644 --- a/crates/io/src/linked.rs +++ b/crates/io/src/linked.rs @@ -50,6 +50,14 @@ impl<'a> Rd<'a> { /// Parses one item; `None` for malformed data or non-`liFD` items. fn parse_item(item: &[u8]) -> Option { + let (uuid, file_name, start, len) = item_header(item)?; + let bytes = item.get(start..start.checked_add(len)?)?.to_vec(); + Some(LinkedFile { uuid, file_name, bytes }) +} + +/// The uuid and file name of an embedded (`liFD`) item, and where its file bytes start and how +/// many there are; `None` for malformed data or other items. +fn item_header(item: &[u8]) -> Option<(String, String, usize, usize)> { let mut r = Rd { d: item, p: 0 }; let kind = r.take(4)?; let _version = r.u32()?; @@ -65,8 +73,26 @@ fn parse_item(item: &[u8]) -> Option { if kind != b"liFD" { return None; } - let bytes = r.take(len)?.to_vec(); - Some(LinkedFile { uuid, file_name, bytes }) + Some((uuid, file_name, r.p, len)) +} + +/// The file name of the embedded file for smart-object `uuid` among the document's preserved PSD +/// global blocks, without copying its bytes: whether a placed layer's source is embedded. +pub fn embedded_file_name(meta: &Metadata, uuid: &str) -> Option { + if uuid.is_empty() { + return None; + } + meta.psd_global_blocks.iter().filter(|(_, k, _)| matches!(k, b"lnk2" | b"lnk3" | b"lnkD")).find_map(|(_, _, d)| { + let mut r = Rd { d, p: 0 }; + while let Some(len) = r.u64() { + let item = r.take(usize::try_from(len).ok()?)?; + if let Some((_, name, ..)) = item_header(item).filter(|h| h.0 == uuid) { + return Some(name); + } + r.p = r.p.next_multiple_of(4); + } + None + }) } /// Every embedded (`liFD`) file in one linked-layer block's data.