mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 05:27:08 -06:00
515 lines
21 KiB
Rust
515 lines
21 KiB
Rust
use super::limits::{Error, Result};
|
|
use super::model::{
|
|
Adjustment, ChannelInfo, ColorMode, Layer, LayerMask, LinkedFile, MAX_GROUP_DEPTH, Node, Rect, Section, SmartObject, SubPath, TextInfo, VectorMask,
|
|
};
|
|
use super::{Psd, Reader, descriptor, engine_data, placed};
|
|
use std::sync::Arc;
|
|
|
|
/// Whether `bytes` start like a Photoshop document.
|
|
pub fn is_psd(bytes: &[u8]) -> bool {
|
|
bytes.len() >= 6 && &bytes[..4] == b"8BPS" && matches!(u16::from_be_bytes([bytes[4], bytes[5]]), 1 | 2)
|
|
}
|
|
|
|
impl Psd {
|
|
pub fn parse(data: impl Into<Arc<[u8]>>) -> Result<Psd> {
|
|
let data: Arc<[u8]> = data.into();
|
|
let mut r = Reader::new(&data);
|
|
if r.bytes(4).ok() != Some(b"8BPS") {
|
|
return Err(Error::NotPsd);
|
|
}
|
|
let version = r.u16()?;
|
|
let psb = match version {
|
|
1 => false,
|
|
2 => true,
|
|
_ => return Err(Error::NotPsd),
|
|
};
|
|
r.skip(6)?;
|
|
let channels = r.u16()?;
|
|
let height = r.u32()?;
|
|
let width = r.u32()?;
|
|
let depth = r.u16()?;
|
|
let mode_v = r.u16()?;
|
|
let mode = ColorMode::from_u16(mode_v).ok_or_else(|| Error::Unsupported(format!("colour mode {mode_v}")))?;
|
|
if !matches!(depth, 1 | 8 | 16 | 32) {
|
|
return Err(Error::Unsupported(format!("{depth}-bit")));
|
|
}
|
|
if width == 0 || height == 0 || width > 300_000 || height > 300_000 {
|
|
return Err(Error::Invalid(format!("size {width}x{height}")));
|
|
}
|
|
// Colour mode data.
|
|
let n = r.u32()? as usize;
|
|
let palette = r.bytes(n)?.to_vec();
|
|
// Image resources.
|
|
let n = r.u32()? as usize;
|
|
r.skip(n)?;
|
|
// Layer and mask information.
|
|
let mut psd = Psd {
|
|
psb,
|
|
width,
|
|
height,
|
|
channels,
|
|
depth,
|
|
mode,
|
|
layers: vec![],
|
|
merged_alpha: false,
|
|
palette,
|
|
linked: vec![],
|
|
warnings: vec![],
|
|
data: data.clone(),
|
|
merged_offset: 0,
|
|
};
|
|
let lm_len = r.length(psb)?;
|
|
let lm_end = r.pos.checked_add(lm_len).filter(|&e| e <= data.len()).ok_or(Error::Truncated)?;
|
|
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
|
|
&& 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 {
|
|
let g = r.u32()? as usize;
|
|
r.pos = (r.pos + g).min(lm_end);
|
|
}
|
|
// Global additional layer information (16/32-bit layer info lives here).
|
|
while let Some((key, start, end)) = next_block(&data, &mut r.pos, lm_end, psb) {
|
|
if matches!(&key, b"lnk2" | b"lnkD" | b"lnk3" | b"lnkE") {
|
|
psd.linked_files(&data, start, end);
|
|
}
|
|
if matches!(&key, b"Lr16" | b"Lr32" | b"Layr") && psd.layers.is_empty() && end > start {
|
|
// The block holds the layer info, normally without its length field; accept
|
|
// both layouts.
|
|
if psd.layer_info(&data, start, end).is_err() || psd.layers.is_empty() {
|
|
psd.layers.clear();
|
|
let skip = if psb { 8 } else { 4 };
|
|
if start + skip < end && psd.layer_info(&data, start + skip, end).is_err() {
|
|
psd.layers.clear();
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
|
|
/// The embedded bytes of linked file `uuid`.
|
|
pub fn linked_data(&self, uuid: &str) -> Option<&[u8]> {
|
|
let f = self.linked.iter().find(|f| f.uuid == uuid)?;
|
|
self.data.get(f.data.clone()?)
|
|
}
|
|
|
|
/// Linked layer data records (spec: "Linked Layer"): length, kind (`liFD` embedded data,
|
|
/// `liFE` external file, `liFA` alias), version, unique id, original file name, type,
|
|
/// creator, data length, optional descriptors, then the data. A damaged record ends the list.
|
|
fn linked_files(&mut self, data: &[u8], start: usize, end: usize) {
|
|
let mut pos = start;
|
|
while pos + 8 <= end {
|
|
let mut r = Reader::at(&data[..end], pos);
|
|
let Ok(len) = r.u64() else { break };
|
|
let body = r.pos;
|
|
let Some(rec_end) = body.checked_add(len as usize).filter(|e| *e <= end) else { break };
|
|
let mut r = Reader::at(&data[..rec_end], body);
|
|
let rec = (|| -> Result<LinkedFile> {
|
|
let kind = r.tag()?;
|
|
let version = r.u32()?;
|
|
let n = r.u8()? as usize;
|
|
let uuid = String::from_utf8_lossy(r.bytes(n)?).into_owned();
|
|
let name = descriptor::read_unicode(&mut r)?;
|
|
let file_type = String::from_utf8_lossy(&r.tag()?).trim().to_string();
|
|
let _creator = r.tag()?;
|
|
let dlen = r.u64()? as usize;
|
|
if r.u8()? != 0 {
|
|
let _v = r.u32()?;
|
|
descriptor::read_descriptor(&mut r)?;
|
|
}
|
|
let mut out = LinkedFile { uuid, name, file_type, data: None };
|
|
match &kind {
|
|
b"liFD" => {
|
|
let s = r.pos;
|
|
r.skip(dlen)?;
|
|
out.data = Some(s..s + dlen);
|
|
}
|
|
b"liFE" => {
|
|
let _v = r.u32()?;
|
|
descriptor::read_descriptor(&mut r)?;
|
|
if version > 3 {
|
|
r.skip(4 + 4 + 8)?;
|
|
}
|
|
let _size = r.u64()?;
|
|
if version > 2 && r.remaining() >= dlen && dlen > 0 {
|
|
let s = r.pos;
|
|
out.data = Some(s..s + dlen);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(out)
|
|
})();
|
|
match rec {
|
|
Ok(f) => self.linked.push(f),
|
|
Err(_) => break,
|
|
}
|
|
pos = rec_end.div_ceil(4) * 4;
|
|
}
|
|
}
|
|
|
|
fn layer_info(&mut self, data: &[u8], start: usize, end: usize) -> Result<()> {
|
|
let mut r = Reader::at(&data[..end], start);
|
|
let count = r.i16()?;
|
|
self.merged_alpha = count < 0;
|
|
let n = count.unsigned_abs() as usize;
|
|
let mut layers = Vec::with_capacity(n);
|
|
for i in 0..n {
|
|
layers.push(self.layer_record(&mut r, i)?);
|
|
}
|
|
// Channel image data follows in record order.
|
|
for l in &mut layers {
|
|
for c in &mut l.channels {
|
|
c.offset = r.pos;
|
|
r.skip(c.len)?;
|
|
}
|
|
}
|
|
self.layers = layers;
|
|
Ok(())
|
|
}
|
|
|
|
fn layer_record(&self, r: &mut Reader, index: usize) -> Result<Layer> {
|
|
let psb = self.psb;
|
|
let (top, left, bottom, right) = (r.i32()?, r.i32()?, r.i32()?, r.i32()?);
|
|
let nch = r.u16()? as usize;
|
|
if nch > 64 {
|
|
return Err(Error::Invalid("channel count".into()));
|
|
}
|
|
let mut channels = Vec::with_capacity(nch);
|
|
for _ in 0..nch {
|
|
let id = r.i16()?;
|
|
let len = r.length(psb)?;
|
|
channels.push(ChannelInfo { id, offset: 0, len });
|
|
}
|
|
if r.bytes(4)? != b"8BIM" {
|
|
return Err(Error::Invalid("layer record signature".into()));
|
|
}
|
|
let blend = r.tag()?;
|
|
let opacity = r.u8()?;
|
|
let clipping = r.u8()? != 0;
|
|
let flags = r.u8()?;
|
|
r.skip(1)?;
|
|
let extra = r.u32()? as usize;
|
|
let extra_end = r.pos.checked_add(extra).filter(|&e| e <= r.data.len()).ok_or(Error::Truncated)?;
|
|
let mut l = Layer {
|
|
index,
|
|
rect: Rect { top, left, bottom, right },
|
|
blend,
|
|
opacity,
|
|
fill_opacity: 255,
|
|
clipping,
|
|
hidden: flags & 2 != 0,
|
|
protect_transparency: flags & 1 != 0,
|
|
channels,
|
|
..Default::default()
|
|
};
|
|
// Layer mask data.
|
|
let mlen = r.u32()? as usize;
|
|
let mstart = r.pos;
|
|
if mlen >= 18 {
|
|
let rect = Rect { top: r.i32()?, left: r.i32()?, bottom: r.i32()?, right: r.i32()? };
|
|
let default_color = r.u8()?;
|
|
let mflags = r.u8()?;
|
|
l.mask = Some(LayerMask { rect, default_color, disabled: mflags & 2 != 0, inverted: mflags & 4 != 0 });
|
|
}
|
|
r.pos = mstart + mlen;
|
|
// Blending ranges.
|
|
let blen = r.u32()? as usize;
|
|
r.skip(blen)?;
|
|
l.name = r.pascal(4)?;
|
|
let mut pos = r.pos;
|
|
let data = r.data;
|
|
while let Some((key, start, end)) = next_block(data, &mut pos, extra_end, psb) {
|
|
l.keys.push(key);
|
|
let block = &data[start..end];
|
|
// A malformed block only loses that block's information.
|
|
let _ = self.layer_block(&mut l, &key, block);
|
|
pos = end;
|
|
}
|
|
r.pos = extra_end;
|
|
Ok(l)
|
|
}
|
|
|
|
fn layer_block(&self, l: &mut Layer, key: &[u8; 4], b: &[u8]) -> Result<()> {
|
|
let mut r = Reader::new(b);
|
|
match key {
|
|
b"luni" => {
|
|
let name = descriptor::read_unicode(&mut r)?;
|
|
if !name.is_empty() {
|
|
l.name = name;
|
|
}
|
|
}
|
|
b"lyid" => l.id = Some(r.u32()?),
|
|
b"iOpa" => l.fill_opacity = r.u8()?,
|
|
b"lsct" | b"lsdk" => {
|
|
l.section = match r.u32()? {
|
|
1 => Section::OpenFolder,
|
|
2 => Section::ClosedFolder,
|
|
3 => Section::Divider,
|
|
_ => Section::Layer,
|
|
};
|
|
if b.len() >= 12 && &b[4..8] == b"8BIM" {
|
|
l.blend.copy_from_slice(&b[8..12]);
|
|
}
|
|
}
|
|
b"TySh" => {
|
|
let _ver = r.u16()?;
|
|
let mut t = [0.0; 6];
|
|
for v in &mut t {
|
|
*v = r.f64()?;
|
|
}
|
|
// A transform with NaN/inf is unusable: fall back to the identity.
|
|
if t.iter().any(|v| !v.is_finite()) {
|
|
t = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
|
|
}
|
|
let _text_ver = r.u16()?;
|
|
let _desc_ver = r.u32()?;
|
|
let d = descriptor::read_descriptor(&mut r)?;
|
|
let text = d.text("Txt ").unwrap_or_default().replace("\r\n", "\n").replace('\r', "\n");
|
|
let text = text.strip_suffix('\n').map(str::to_string).unwrap_or(text);
|
|
let style = d.raw("EngineData").map(|e| engine_data::text_style(&engine_data::parse(e))).unwrap_or_default();
|
|
let bounds =
|
|
d.obj("bounds").or_else(|| d.obj("boundingBox")).and_then(|o| Some([o.num("Left")?, o.num("Top ")?, o.num("Rght")?, o.num("Btom")?]));
|
|
l.text = Some(TextInfo { text, transform: t, style, bounds });
|
|
}
|
|
b"lfx2" => {
|
|
let _ver = r.u32()?;
|
|
let _dver = r.u32()?;
|
|
l.effects = Some(descriptor::read_descriptor(&mut r)?);
|
|
}
|
|
b"SoCo" => {
|
|
let _ver = r.u32()?;
|
|
let d = descriptor::read_descriptor(&mut r)?;
|
|
l.fill = d.color("Clr ");
|
|
}
|
|
b"nvrt" => l.adjustment = Some(Adjustment::Invert),
|
|
b"brit" => {
|
|
let brightness = r.i16()? as f64;
|
|
let contrast = r.i16()? as f64;
|
|
if l.adjustment.is_none() {
|
|
l.adjustment = Some(Adjustment::BrightnessContrast { brightness, contrast, legacy: true });
|
|
}
|
|
}
|
|
b"CgEd" => {
|
|
let _ver = r.u32()?;
|
|
let d = descriptor::read_descriptor(&mut r)?;
|
|
l.adjustment = Some(Adjustment::BrightnessContrast {
|
|
brightness: d.num("Brgh").unwrap_or(0.0),
|
|
contrast: d.num("Cntr").unwrap_or(0.0),
|
|
legacy: d.bool("useLegacy").unwrap_or(false),
|
|
});
|
|
}
|
|
b"hue2" | b"hue " => {
|
|
let _ver = r.u16()?;
|
|
let colorize = r.u8()? != 0;
|
|
r.skip(1)?;
|
|
let ch = (r.i16()? as f64, r.i16()? as f64, r.i16()? as f64);
|
|
let m = (r.i16()? as f64, r.i16()? as f64, r.i16()? as f64);
|
|
let (hue, saturation, lightness) = if colorize { ch } else { m };
|
|
l.adjustment = Some(Adjustment::HueSaturation { hue, saturation, lightness, colorize });
|
|
}
|
|
b"levl" => {
|
|
let _ver = r.u16()?;
|
|
let (ib, iw, ob, ow, g) = (r.i16()?, r.i16()?, r.i16()?, r.i16()?, r.i16()?);
|
|
l.adjustment =
|
|
Some(Adjustment::Levels { in_black: ib as f64, in_white: iw as f64, gamma: g as f64 / 100.0, out_black: ob as f64, out_white: ow as f64 });
|
|
}
|
|
b"expA" => {
|
|
let _ver = r.u16()?;
|
|
let mut finite = |default: f64| -> Result<f64> {
|
|
let v = f64::from(r.f32()?);
|
|
Ok(if v.is_finite() { v } else { default })
|
|
};
|
|
let exposure = finite(0.0)?;
|
|
let offset = finite(0.0)?;
|
|
let gamma = finite(1.0)?;
|
|
l.adjustment = Some(Adjustment::Exposure { exposure, offset, gamma });
|
|
}
|
|
b"curv" | b"blnc" | b"post" | b"thrs" | b"selc" | b"mixr" | b"phfl" | b"vibA" | b"blwh" | b"grdm" | b"clrL" if l.adjustment.is_none() => {
|
|
l.adjustment = Some(Adjustment::Other(String::from_utf8_lossy(key).into_owned()));
|
|
}
|
|
b"SoLd" | b"SoLE" => {
|
|
let _id = r.tag()?;
|
|
let _ver = r.u32()?;
|
|
let _dver = r.u32()?;
|
|
let d = descriptor::read_descriptor(&mut r)?;
|
|
let uuid = d.text("Idnt").unwrap_or_default().to_string();
|
|
let t = placed::eight(&d, "Trnf").unwrap_or([0.0; 8]);
|
|
// The non-affine transform when present (perspective), else the transform.
|
|
let t = placed::eight(&d, "nonAffineTransform").unwrap_or(t);
|
|
let size = d.obj("Sz ").and_then(|s| Some([s.num("Wdth")?, s.num("Hght")?]));
|
|
let warp = d.obj("warp").and_then(placed::Warp::read);
|
|
l.smart_object = Some(SmartObject { uuid, quad: placed::quad_from(t), size, warp });
|
|
}
|
|
b"PlLd" if l.smart_object.is_none() => {
|
|
let _id = r.tag()?;
|
|
let _ver = r.u32()?;
|
|
let n = r.u8()? as usize;
|
|
let uuid = String::from_utf8_lossy(r.bytes(n)?).into_owned();
|
|
let _page = r.u32()?;
|
|
let _pages = r.u32()?;
|
|
let _aa = r.u32()?;
|
|
let _ty = r.u32()?;
|
|
let mut t = [0.0; 8];
|
|
for v in &mut t {
|
|
*v = r.f64_finite()?;
|
|
}
|
|
l.smart_object = Some(SmartObject { uuid, quad: placed::quad_from(t), size: None, warp: None });
|
|
}
|
|
b"vmsk" | b"vsms" => {
|
|
let _ver = r.u32()?;
|
|
let flags = r.u32()?;
|
|
l.vector_mask = Some(self.vector_paths(&mut r, flags)?);
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn vector_paths(&self, r: &mut Reader, flags: u32) -> Result<VectorMask> {
|
|
let (w, h) = (self.width as f64, self.height as f64);
|
|
let mut vm = VectorMask { inverted: flags & 1 != 0, disabled: flags & 4 != 0, subpaths: vec![] };
|
|
let fixed = |r: &mut Reader| -> Result<f64> { Ok(r.i32()? as f64 / (1 << 24) as f64) };
|
|
while r.remaining() >= 26 {
|
|
let sel = r.u16()?;
|
|
match sel {
|
|
0 | 3 => {
|
|
let _n = r.u16()?;
|
|
r.skip(22)?;
|
|
vm.subpaths.push(SubPath { closed: sel == 0, knots: vec![] });
|
|
}
|
|
1 | 2 | 4 | 5 => {
|
|
let mut pts = [[0.0; 2]; 3];
|
|
for p in &mut pts {
|
|
let y = fixed(r)?;
|
|
let x = fixed(r)?;
|
|
*p = [x * w, y * h];
|
|
}
|
|
if let Some(sp) = vm.subpaths.last_mut() {
|
|
sp.knots.push(pts);
|
|
}
|
|
}
|
|
_ => r.skip(24)?,
|
|
}
|
|
}
|
|
vm.subpaths.retain(|s| !s.knots.is_empty());
|
|
Ok(vm)
|
|
}
|
|
|
|
/// The layer tree, top of the stack first. Groups nest at most [`MAX_GROUP_DEPTH`] deep (see
|
|
/// [`Psd::tree_report`]).
|
|
pub fn tree(&self) -> Vec<Node> {
|
|
self.tree_report().0
|
|
}
|
|
|
|
/// The layer tree plus the number of groups nested deeper than [`MAX_GROUP_DEPTH`]. Those
|
|
/// are flattened into their deepest allowed ancestor: their children stay, in order, and
|
|
/// the group record itself becomes a plain [`Node::Layer`], so no layer is lost and the
|
|
/// tree (and its `Drop`, `Clone` and `PartialEq`) cannot recurse deeper than the limit.
|
|
pub fn tree_report(&self) -> (Vec<Node>, usize) {
|
|
let mut stack: Vec<Vec<Node>> = vec![vec![]];
|
|
// Dividers seen while `MAX_GROUP_DEPTH` groups were already open.
|
|
let mut flattened_open = 0usize;
|
|
let mut flattened = 0usize;
|
|
for l in &self.layers {
|
|
match l.section {
|
|
Section::Divider => {
|
|
if stack.len() > MAX_GROUP_DEPTH {
|
|
flattened_open += 1;
|
|
} else {
|
|
stack.push(vec![]);
|
|
}
|
|
}
|
|
Section::OpenFolder | Section::ClosedFolder => {
|
|
if flattened_open > 0 {
|
|
flattened_open -= 1;
|
|
flattened += 1;
|
|
if let Some(top) = stack.last_mut() {
|
|
top.push(Node::Layer(l.index));
|
|
}
|
|
continue;
|
|
}
|
|
let children = if stack.len() > 1 { stack.pop().unwrap_or_default() } else { vec![] };
|
|
if let Some(top) = stack.last_mut() {
|
|
top.push(Node::Group { layer: l.index, children });
|
|
}
|
|
}
|
|
Section::Layer => {
|
|
if let Some(top) = stack.last_mut() {
|
|
top.push(Node::Layer(l.index));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
while stack.len() > 1 {
|
|
let c = stack.pop().unwrap_or_default();
|
|
if let Some(top) = stack.last_mut() {
|
|
top.extend(c);
|
|
}
|
|
}
|
|
// Depth is at most `MAX_GROUP_DEPTH`, so recursion is bounded.
|
|
fn rev(v: &mut Vec<Node>) {
|
|
v.reverse();
|
|
for n in v {
|
|
if let Node::Group { children, .. } = n {
|
|
rev(children);
|
|
}
|
|
}
|
|
}
|
|
let mut out = stack.pop().unwrap_or_default();
|
|
rev(&mut out);
|
|
(out, flattened)
|
|
}
|
|
}
|
|
|
|
/// The next additional-information block at `*pos` (signature `8BIM`/`8B64`, key, length):
|
|
/// returns (key, data start, data end). Skips up to 3 padding bytes before a signature.
|
|
fn next_block(data: &[u8], pos: &mut usize, end: usize, psb: bool) -> Option<([u8; 4], usize, usize)> {
|
|
let mut p = *pos;
|
|
for _ in 0..4 {
|
|
if p + 12 > end {
|
|
return None;
|
|
}
|
|
if &data[p..p + 4] == b"8BIM" || &data[p..p + 4] == b"8B64" {
|
|
break;
|
|
}
|
|
p += 1;
|
|
}
|
|
if p + 12 > end || !(&data[p..p + 4] == b"8BIM" || &data[p..p + 4] == b"8B64") {
|
|
return None;
|
|
}
|
|
let mut key = [0u8; 4];
|
|
key.copy_from_slice(&data[p + 4..p + 8]);
|
|
let wide =
|
|
psb && matches!(&key, b"LMsk" | b"Lr16" | b"Lr32" | b"Layr" | b"Mt16" | b"Mt32" | b"Mtrn" | b"Alph" | b"FMsk" | b"lnk2" | b"FEid" | b"FXid" | b"PxSD");
|
|
let mut r = Reader::at(&data[..end], p + 8);
|
|
let len = r.length(wide).ok()?;
|
|
let start = r.pos;
|
|
let stop = start.checked_add(len)?;
|
|
if stop > end {
|
|
return None;
|
|
}
|
|
*pos = stop;
|
|
Some((key, start, stop))
|
|
}
|