mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-29 12:27:38 -06:00
PSD/PSB: a 16/32-bit file's "Transparency" channel stays the merged transparency on re-save (#4654)
Photoshop's 16-bit PSBs can mark the merged transparency only by naming it "Transparency" in the alpha channel names (positive Lr16 layer count, no Mt16). The importer read it as a user alpha channel, and the next save wrote a second one. merged_has_alpha now also accepts that name when the layers sit in a global Lr16/Lr32 block and the names cover every extra channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2a952f164c
commit
cd75fa78ef
Binary file not shown.
@@ -226,11 +226,11 @@ fn mt32_marks_the_merged_transparency() {
|
||||
let plane = |v: [f32; 2]| v.into_iter().flat_map(f32::to_be_bytes).collect::<Vec<u8>>();
|
||||
file.image_data.data = [plane([0.0, 1.0]), plane([0.0, 1.0]), plane([0.0, 1.0]), plane([1.0, 0.0])].concat();
|
||||
let name: Vec<u8> = [0u8, 0, 0, 13].into_iter().chain("Transparency\0".encode_utf16().flat_map(u16::to_be_bytes)).collect();
|
||||
file.resources.retain(|r| r.id != 1045);
|
||||
file.resources.push(photocraft_psd::ImageResource::new(1045, name));
|
||||
file.resources.retain(|r| !matches!(r.id, 1006 | 1045));
|
||||
assert!(!file.merged_has_alpha(), "a positive layer count alone");
|
||||
file.global_blocks.push(photocraft_psd::TaggedBlock::new(*b"Mt32", Vec::new()));
|
||||
assert!(file.merged_has_alpha());
|
||||
file.resources.push(photocraft_psd::ImageResource::new(1045, name));
|
||||
let merged = merged_composite(&file).unwrap();
|
||||
assert_eq!(merged.iter().map(|p| p[3]).collect::<Vec<_>>(), vec![1.0, 0.0]);
|
||||
let opened = import("transparent.psd", &file.to_bytes().unwrap()).unwrap();
|
||||
|
||||
+34
-2
@@ -354,7 +354,10 @@ impl PsdFile {
|
||||
/// `true` if the merged image carries a transparency channel: its first extra channel, when
|
||||
/// the layer count was stored negative or a "Saving Merged Transparency" global block (`Mtrn`,
|
||||
/// `Mt16`, `Mt32`) is present. Photoshop marks 32-bit documents with `Mt32` and a positive
|
||||
/// layer count (psd-tools transparentbg.psd).
|
||||
/// layer count (psd-tools transparentbg.psd). Photoshop also names the merged transparency
|
||||
/// "Transparency" and lists it first among the alpha channel names, and some of its 16-bit
|
||||
/// PSBs carry no other mark (#4654): when the layers sit in a global `Lr16`/`Lr32` block, that
|
||||
/// name with one name per extra channel marks it too.
|
||||
pub fn merged_has_alpha(&self) -> bool {
|
||||
let Some(color) = self.header.color_mode.color_channels() else {
|
||||
return false;
|
||||
@@ -364,11 +367,40 @@ impl PsdFile {
|
||||
}
|
||||
let marked = self.global_blocks.iter().any(|b| matches!(&b.key, b"Mtrn" | b"Mt16" | b"Mt32"));
|
||||
match &self.layer_info {
|
||||
Some(li) => li.merged_alpha || marked,
|
||||
Some(li) => {
|
||||
let in_block = matches!(self.layer_info_placement, LayerInfoPlacement::GlobalBlock { .. });
|
||||
li.merged_alpha || marked || (in_block && self.names_merged_transparency(self.header.channels - color))
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` when the alpha channel names (resource 1045, else 1006) hold exactly `extra` names
|
||||
/// and the first is "Transparency", Photoshop's name for the merged transparency.
|
||||
fn names_merged_transparency(&self, extra: u16) -> bool {
|
||||
const NAME: &str = "Transparency";
|
||||
let mut names = Vec::new();
|
||||
if let Some(r) = self.resource(1045) {
|
||||
let mut at = 0usize;
|
||||
while let Some(len) = r.data.get(at..at.saturating_add(4)).and_then(|b| b.try_into().ok()).map(u32::from_be_bytes) {
|
||||
at = at.saturating_add(4);
|
||||
let end = (len as usize).checked_mul(2).and_then(|n| at.checked_add(n));
|
||||
let Some(units) = end.and_then(|end| r.data.get(at..end)) else { break };
|
||||
let units: Vec<u16> = units.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes(*c)).collect();
|
||||
names.push(String::from_utf16_lossy(&units).trim_end_matches('\0').to_string());
|
||||
at = at.saturating_add(units.len() * 2);
|
||||
}
|
||||
} else if let Some(r) = self.resource(1006) {
|
||||
let mut at = 0usize;
|
||||
while let Some(&len) = r.data.get(at) {
|
||||
let Some(name) = r.data.get(at + 1..at + 1 + usize::from(len)) else { break };
|
||||
names.push(String::from_utf8_lossy(name).into_owned());
|
||||
at += 1 + usize::from(len);
|
||||
}
|
||||
}
|
||||
names.len() == usize::from(extra) && names.first().is_some_and(|n| n == NAME)
|
||||
}
|
||||
|
||||
/// Checks structural invariants that the writer relies on.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
self.header.validate()?;
|
||||
|
||||
Reference in New Issue
Block a user