mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 03:57:44 -06:00
#1132: a damaged PSD layer section keeps the merged image instead of failing the document
Psd::parse drops the layers, records Psd::warnings and keeps the header and merged image; the importer then imports the flattened image as one layer and reports 'Layers could not be read'. Header damage and a missing merged image stay hard errors. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
470ea7a3af
commit
6259bf0b57
@@ -111,10 +111,45 @@ struct Ctx<'a> {
|
||||
pub fn import(proj: &mut Project, psd: &Psd, path: &str, name: &str, retain: bool, rate: FrameRate, duration: Tick) -> PsdImport {
|
||||
let folder = proj.add_item(&format!("{name} Layers"), Label::Yellow, None, ItemKind::Folder);
|
||||
let mut cx = Ctx { psd, path, retain, folder, rate, duration, items: vec![], warnings: vec![] };
|
||||
let comp = build_comp(proj, &mut cx, name, &psd.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, &psd.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());
|
||||
|
||||
@@ -283,3 +283,30 @@ fn paste_svg_rejects_text_that_is_not_usable_svg() {
|
||||
assert!(s.execute_checked("edit.pasteSvg", json!({})).is_err());
|
||||
assert_eq!(s.active_comp().unwrap().layers.len(), 1, "nothing pasted");
|
||||
}
|
||||
|
||||
#[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
@@ -339,6 +339,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,
|
||||
}
|
||||
@@ -387,6 +390,7 @@ impl Psd {
|
||||
merged_alpha: false,
|
||||
palette,
|
||||
linked: vec![],
|
||||
warnings: vec![],
|
||||
data: data.clone(),
|
||||
merged_offset: 0,
|
||||
};
|
||||
@@ -395,8 +399,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 {
|
||||
@@ -422,6 +435,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)
|
||||
}
|
||||
|
||||
@@ -252,3 +252,58 @@ fn huge_layer_bounds_error_instead_of_allocating() {
|
||||
let p = Psd::parse(bytes).unwrap();
|
||||
assert!(matches!(p.layer_pixels(0, false), Err(Error::Unsupported(_))));
|
||||
}
|
||||
|
||||
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