mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
info: a smart object embedded in a PSD is reported as embedded (#4843)
PSD import keeps every placed layer as SmartSource::Linked { path:
<Idnt uuid> } so duplicates share the file and export stays
byte-stable, and info reported that as a link whose path is the uuid.
When the uuid has an embedded file (a liFD item in the preserved
lnk2/lnk3/lnkD blocks), the document's info now says
{"kind": "embedded", "fileName", "uuid"}; the new
linked::embedded_file_name finds it without copying the file.
Fixes #4842
This commit is contained in:
@@ -38,7 +38,7 @@ pub fn document(d: &DocState) -> Value {
|
||||
"isolatedLayers": d.isolated_layers.iter().map(|l| l.0).collect::<Vec<_>>(),
|
||||
"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::<Vec<_>>(),
|
||||
"layers": doc.layers.iter().rev().map(|l| layer_sel(l, &selected, Some(&doc.metadata))).collect::<Vec<_>>(),
|
||||
"history": d.history.entries(),
|
||||
"snapshots": d.snapshots.iter().map(|snap| json!({"id": snap.id, "name": snap.name})).collect::<Vec<_>>(),
|
||||
"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) => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
+28
-2
@@ -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<LinkedFile> {
|
||||
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<LinkedFile> {
|
||||
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<String> {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user