mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #1135 from vangel76/fix-psd-lenient-layer-section
# Conflicts: # crates/engine/src/psd_import.rs # crates/engine/src/tests_vector_import.rs # crates/psd/src/tests.rs
This commit is contained in:
commit
b438cdd702
4 files changed
+137
-3
No files matched your search
@@ -115,10 +115,45 @@ pub fn import(proj: &mut Project, psd: &Psd, path: &str, name: &str, retain: boo
|
||||
if flattened > 0 {
|
||||
cx.warnings.push(format!("{flattened} group(s) nested deeper than {} levels were flattened into their parent", effectcraft_psd::MAX_GROUP_DEPTH));
|
||||
}
|
||||
let comp = build_comp(proj, &mut cx, name, &tree, None);
|
||||
cx.warnings.extend(psd.warnings.iter().cloned());
|
||||
let comp = if psd.layers.is_empty() && !psd.warnings.is_empty() {
|
||||
flattened_comp(proj, &mut cx, name)
|
||||
} else {
|
||||
build_comp(proj, &mut cx, name, &tree, None)
|
||||
};
|
||||
PsdImport { comp, folder, items: cx.items, warnings: cx.warnings }
|
||||
}
|
||||
|
||||
/// The layers could not be read: the merged image as the composition's only layer.
|
||||
fn flattened_comp(proj: &mut Project, cx: &mut Ctx, name: &str) -> ItemId {
|
||||
let cid = proj.add_item(name, Label::Sandstone, None, ItemKind::Comp(Arc::new(new_comp(cx))));
|
||||
cx.items.push(cid);
|
||||
let mut comp = new_comp(cx);
|
||||
let file = std::path::Path::new(cx.path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
||||
let f = Footage {
|
||||
path: cx.path.to_string(),
|
||||
kind: FootageKind::Still,
|
||||
width: cx.psd.width,
|
||||
height: cx.psd.height,
|
||||
pixel_aspect: 1.0,
|
||||
frame_rate: cx.rate,
|
||||
has_video: true,
|
||||
alpha: if cx.psd.merged_alpha { AlphaMode::Straight } else { AlphaMode::Ignore },
|
||||
loop_count: 1,
|
||||
codec: "PSD".into(),
|
||||
layer: None,
|
||||
..Default::default()
|
||||
};
|
||||
let id = proj.add_item(&file, Label::Lavender, Some(cx.folder), ItemKind::Footage(f));
|
||||
cx.items.push(id);
|
||||
let l = build::layer(proj, &comp, name, LayerSource::Footage { item: id }, (cx.psd.width, cx.psd.height), None);
|
||||
comp.layers.push(l);
|
||||
if let Some(item) = proj.item_mut(cid) {
|
||||
item.kind = ItemKind::Comp(Arc::new(comp));
|
||||
}
|
||||
cid
|
||||
}
|
||||
|
||||
fn new_comp(cx: &Ctx) -> Comp {
|
||||
// A PSB can be 300,000 px a side; a composition is at most 30,000 (#788).
|
||||
let size = |v: u32| effectcraft_project::limits::size(v.into());
|
||||
|
||||
@@ -310,3 +310,30 @@ fn psd_with_thousands_of_nested_groups_imports_on_a_small_stack() {
|
||||
assert_eq!(comps, 65);
|
||||
assert!(warned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn psd_with_damaged_layer_section_imports_the_flattened_image() {
|
||||
let mut d = WDoc::new(30, 20);
|
||||
d.layers = vec![
|
||||
WLayer::solid("Back", Rect::new(0, 0, 30, 20), [0.0, 0.0, 1.0, 1.0]),
|
||||
WLayer::solid("Box", Rect::new(2, 2, 8, 8), [1.0, 0.0, 0.0, 1.0]).with_blend(b"mul "),
|
||||
];
|
||||
let mut bytes = write(&d);
|
||||
let at = bytes.windows(8).position(|w| w == b"8BIMmul ").unwrap();
|
||||
bytes[at] = b'X';
|
||||
let path = tmp("damaged.psd");
|
||||
std::fs::write(&path, bytes).unwrap();
|
||||
let mut s = Session::default();
|
||||
let r = s.execute_checked("file.import", json!({"paths": [path], "importAs": "composition"})).unwrap();
|
||||
// Photoshop import warnings are reported with the import's `errors` (as for adjustment layers).
|
||||
let warnings = r["errors"].as_array().unwrap();
|
||||
assert_eq!(warnings.len(), 1, "{r}");
|
||||
assert!(warnings[0].as_str().unwrap().contains("Layers could not be read"), "{r}");
|
||||
let cid = effectcraft_project::ItemId(r["comps"][0].as_u64().unwrap());
|
||||
let c = s.project.comp(cid).unwrap().clone();
|
||||
assert_eq!((c.width, c.height), (30, 20));
|
||||
assert_eq!(c.layers.len(), 1);
|
||||
let LayerSource::Footage { item } = c.layers[0].source else { panic!("footage") };
|
||||
let ItemKind::Footage(f) = &s.project.item(item).unwrap().kind else { panic!() };
|
||||
assert!(f.layer.is_none(), "the merged image, not a Photoshop layer");
|
||||
}
|
||||
+19
-2
@@ -343,6 +343,9 @@ pub struct Psd {
|
||||
pub palette: Vec<u8>,
|
||||
/// Files embedded for smart objects.
|
||||
pub linked: Vec<LinkedFile>,
|
||||
/// Repairs made while reading: damage that was skipped instead of failing the document
|
||||
/// (for example an unreadable layer section, in which case only the merged image is kept).
|
||||
pub warnings: Vec<String>,
|
||||
data: Arc<[u8]>,
|
||||
merged_offset: usize,
|
||||
}
|
||||
@@ -391,6 +394,7 @@ impl Psd {
|
||||
merged_alpha: false,
|
||||
palette,
|
||||
linked: vec![],
|
||||
warnings: vec![],
|
||||
data: data.clone(),
|
||||
merged_offset: 0,
|
||||
};
|
||||
@@ -399,8 +403,17 @@ impl Psd {
|
||||
if lm_len > 0 {
|
||||
let li_len = r.length(psb)?;
|
||||
let li_end = r.pos.checked_add(li_len).filter(|&e| e <= lm_end).ok_or(Error::Truncated)?;
|
||||
if li_len > 0 {
|
||||
psd.layer_info(&data, r.pos, li_end)?;
|
||||
if li_len > 0
|
||||
&& let Err(e) = psd.layer_info(&data, r.pos, li_end)
|
||||
{
|
||||
// A damaged layer section must not discard the document: keep the header and the
|
||||
// merged image, drop the layers and say so. Without a merged image there is
|
||||
// nothing to fall back to, so the original error stands.
|
||||
if lm_end.checked_add(2).is_none_or(|n| n > data.len()) {
|
||||
return Err(e);
|
||||
}
|
||||
psd.layers.clear();
|
||||
psd.warnings.push(format!("Layers could not be read ({e}); only the flattened image is available"));
|
||||
}
|
||||
r.pos = li_end;
|
||||
if r.pos + 4 <= lm_end {
|
||||
@@ -426,6 +439,10 @@ impl Psd {
|
||||
r.pos = end;
|
||||
}
|
||||
}
|
||||
if !psd.layers.is_empty() {
|
||||
// A 16/32-bit layer block recovered the layers.
|
||||
psd.warnings.clear();
|
||||
}
|
||||
psd.merged_offset = lm_end;
|
||||
Ok(psd)
|
||||
}
|
||||
|
||||
@@ -434,3 +434,58 @@ fn rle_composite_matches_the_raw_one() {
|
||||
assert_eq!(rle.layer_pixels(2, true).unwrap().data, raw.layer_pixels(2, true).unwrap().data, "{depth} bit");
|
||||
}
|
||||
}
|
||||
|
||||
fn find(bytes: &[u8], needle: &[u8]) -> usize {
|
||||
bytes.windows(needle.len()).position(|w| w == needle).expect("needle present")
|
||||
}
|
||||
|
||||
/// The second layer record's signature overwritten.
|
||||
fn bad_signature() -> Vec<u8> {
|
||||
let mut bytes = write(&doc_with_layers(8, ColorMode::Rgb, true));
|
||||
let at = find(&bytes, b"8BIMmul ");
|
||||
bytes[at] = b'X';
|
||||
bytes
|
||||
}
|
||||
|
||||
/// The first channel of the first layer claims more data than the file holds.
|
||||
fn bad_channel_length() -> Vec<u8> {
|
||||
let mut bytes = write(&doc_with_layers(8, ColorMode::Rgb, true));
|
||||
let at = find(&bytes, b"8BIMnorm");
|
||||
let first_len = at - 4 * 6 + 2;
|
||||
bytes[first_len..first_len + 4].copy_from_slice(&0x7fff_fff0u32.to_be_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_layer_section_keeps_the_merged_image() {
|
||||
let good = Psd::parse(write(&doc_with_layers(8, ColorMode::Rgb, true))).unwrap();
|
||||
assert!(good.warnings.is_empty() && good.layers.len() == 3);
|
||||
let want = good.composite().unwrap();
|
||||
for bytes in [bad_signature(), bad_channel_length()] {
|
||||
let p = Psd::parse(bytes).unwrap();
|
||||
assert!(p.layers.is_empty());
|
||||
assert_eq!(p.warnings.len(), 1, "{:?}", p.warnings);
|
||||
assert!(p.warnings[0].contains("Layers could not be read"));
|
||||
assert_eq!((p.width, p.height), (40, 30));
|
||||
let got = p.composite().unwrap();
|
||||
assert_eq!(got.data, want.data);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_damage_and_missing_merged_image_still_fail() {
|
||||
let good = write(&doc_with_layers(8, ColorMode::Rgb, true));
|
||||
// Zero width.
|
||||
let mut b = good.clone();
|
||||
b[18..22].copy_from_slice(&0u32.to_be_bytes());
|
||||
assert!(Psd::parse(b).is_err());
|
||||
// Unknown colour mode.
|
||||
let mut b = good.clone();
|
||||
b[24..26].copy_from_slice(&99u16.to_be_bytes());
|
||||
assert!(Psd::parse(b).is_err());
|
||||
// Damaged layers and no merged image at all: nothing to fall back to.
|
||||
let end = Psd::parse(good).unwrap().merged_offset;
|
||||
let mut b = bad_signature();
|
||||
b.truncate(end);
|
||||
assert!(Psd::parse(b).is_err());
|
||||
}
|
||||
Reference in new issue
Block a user