#3087: an item made from one Photoshop layer shows that layer

Footage made from one layer of a layered .psd showed the whole document and
was named after the file. Its opti (tag 8BPS) stores the layer: the record
index (BE i32 at 14, -1 for the merged image), the bounds (LE i32 at 78-93)
and the name (UTF-8 at 344). The item now shows that layer as EffectCraft's
own Photoshop import does (at its own size with Retain Layer Sizes), named
<layer>/<file>. A layer without pixels (an adjustment layer) keeps the merged
image and is reported.
This commit is contained in:
arnav-hiwarkar
2026-11-11 16:56:00 +05:30
parent 6aca5e473c
commit e2e3413011
9 changed files with 180 additions and 14 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ pub use camera::{CameraPosition, DEFAULT_CAMERA_ZOOM};
pub use chunk::{AepFile, Body, Chunk, LIST, NESTED_IDS, Walk};
pub use effects::{DefinitionSource, Effect, EffectDefinitions, EffectParam, LayerRef, ParamKind, ParamValue};
pub use error::AepError;
pub use footage::{Footage, SequenceInfo};
pub use footage::{Footage, PsdLayer, SequenceInfo};
pub use header::{BlendMode, CompHeader, LayerHeader, LayerType, LightType, Quality, Rational, Renderer};
pub use keys::{ExpressionFlags, Interpolation, KeyEase, KeyframeKind, SpatialKey};
pub use masks::{Mask, MaskAtom, MaskMode, PathVertex, ShapePath};
+76 -2
View File
@@ -1,7 +1,7 @@
//! Item ids and labels, footage items (solids and files), precomp linkage, and the name and size
//! of a layer's source.
use crate::container::bytes::{be_f32, be_f64, be_u32, cstr, le_u32, text};
use crate::container::bytes::{be_f32, be_f64, be_i32, be_u32, cstr, le_u32, text, until_nul};
use crate::container::chunk::Chunk;
use crate::container::header::Rational;
use crate::container::typed::{Item, ItemKind, Layer, Project};
@@ -45,6 +45,48 @@ pub struct Footage {
/// True when the alias points at a folder (`"target_is_folder":true`): an image sequence is ONE
/// item whose alias is its folder and whose `opti` holds the first file name.
pub is_sequence: bool,
/// The one layer of a Photoshop file the item shows; `None` for the merged image and for
/// any other footage.
pub psd_layer: Option<PsdLayer>,
}
/// One layer of a Photoshop file shown by a footage item, from its `opti` (tag `8BPS`,
/// docs/aep-format.md 3.3): the layer record's index as a big-endian `i32` at byte 14 (-1 for the
/// merged image), its bounds top, left, bottom, right as little-endian `i32` at 78, 82, 86 and
/// 90, and its name (UTF-8, NUL terminated) from byte 344.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PsdLayer {
/// The layer record's position in the file (bottom of the stack = 0).
pub index: u32,
pub name: String,
/// Top, left, bottom, right in document pixels; all 0 for a layer without pixels (an
/// adjustment layer).
pub bounds: [i32; 4],
}
/// Most bytes of a Photoshop layer name read.
const MAX_NAME: usize = 1024;
impl PsdLayer {
/// The bounds' width and height; `None` when they are empty.
pub fn size(&self) -> Option<(u32, u32)> {
let [top, left, bottom, right] = self.bounds;
let w = u32::try_from(i64::from(right) - i64::from(left)).ok().filter(|w| *w > 0)?;
let h = u32::try_from(i64::from(bottom) - i64::from(top)).ok().filter(|h| *h > 0)?;
Some((w, h))
}
/// The layer an `opti` names; `None` for another tag, the merged image or a cut record.
fn read(opti: &[u8]) -> Option<Self> {
if opti.get(..4) != Some(&b"8BPS"[..]) {
return None;
}
let index = u32::try_from(be_i32(opti, 14)?).ok()?;
let le_i32 = |at| le_u32(opti, at).map(|v| i32::from_le_bytes(v.to_le_bytes()));
// (After Effects writes 602 bytes; a longer hostile name is cut.)
let name = opti.get(344..).map(|b| text(until_nul(b.get(..MAX_NAME).unwrap_or(b)))).unwrap_or_default();
Some(Self { index, name, bounds: [le_i32(78)?, le_i32(82)?, le_i32(86)?, le_i32(90)?] })
}
}
impl Footage {
@@ -330,6 +372,7 @@ impl<'f, 'a> Item<'f, 'a> {
file_size: be_u32(sspc, 208).unwrap_or(0),
audio_sample_rate: be_f64(sspc, 160).filter(|r| r.is_finite() && *r > 0.0),
is_sequence,
psd_layer: PsdLayer::read(opti),
})
}
@@ -370,7 +413,38 @@ impl<'f, 'a> Project<'f, 'a> {
#[cfg(test)]
mod tests {
use super::json_string;
use super::{PsdLayer, json_string};
/// A 602-byte Photoshop `opti` naming layer `index` with `bounds` and `name`.
fn opti(index: i32, bounds: [i32; 4], name: &[u8]) -> Vec<u8> {
let mut b = vec![0u8; 602];
b[..4].copy_from_slice(b"8BPS");
b[14..18].copy_from_slice(&index.to_be_bytes());
for (i, v) in bounds.iter().enumerate() {
b[78 + 4 * i..82 + 4 * i].copy_from_slice(&v.to_le_bytes());
}
b[344..344 + name.len()].copy_from_slice(name);
b
}
#[test]
fn a_photoshop_layer_reads_its_index_bounds_and_name() {
let l = PsdLayer::read(&opti(7, [749, 800, 764, 848], "tést".as_bytes())).unwrap();
assert_eq!((l.index, l.name.as_str(), l.bounds, l.size()), (7, "tést", [749, 800, 764, 848], Some((48, 15))));
// The merged image, another tag, a cut record: no layer.
assert_eq!(PsdLayer::read(&opti(-1, [0; 4], b"x")), None);
let mut other = opti(1, [0; 4], b"x");
other[..4].copy_from_slice(b"Soli");
assert_eq!(PsdLayer::read(&other), None);
assert_eq!(PsdLayer::read(&opti(1, [0; 4], b"x")[..80]), None);
// A cut name, and no name at all.
assert_eq!(PsdLayer::read(&opti(1, [0; 4], b"x")[..345]).unwrap().name, "x");
assert_eq!(PsdLayer::read(&opti(1, [0; 4], b"x")[..300]).unwrap().name, "");
// A layer without pixels has no size; extreme bounds do not overflow.
assert_eq!(PsdLayer::read(&opti(2, [0; 4], b"")).unwrap().size(), None);
assert_eq!(PsdLayer::read(&opti(2, [i32::MIN, i32::MIN, i32::MAX, i32::MAX], b"")).unwrap().size(), Some((u32::MAX, u32::MAX)));
assert_eq!(PsdLayer::read(&opti(2, [i32::MAX, 0, i32::MIN, 1], b"")).unwrap().size(), None);
}
#[test]
fn reads_a_string_with_escapes() {
+26 -2
View File
@@ -12,7 +12,8 @@ use effectcraft_color::{BlendMode, Label};
use effectcraft_keyframe::Value;
use effectcraft_project::build::{self, Ids};
use effectcraft_project::{
AutoOrient, Comp, Footage, FootageKind, ItemId, ItemKind, Layer, LayerId, LayerSource, LightKind, MatteKind, Project, Quality, Solid, TrackMatte,
AutoOrient, Comp, Footage, FootageKind, ItemId, ItemKind, Layer, LayerId, LayerSource, LightKind, MatteKind, Project, Quality, Solid, SourceLayer,
TrackMatte,
};
use effectcraft_time::Tick;
@@ -216,7 +217,17 @@ fn import_footage(project: &mut Project, cx: &mut Ctx<'_>, item: &AepItem<'_, '_
let aep_id = item.id();
// An item without a stored name shows its file's name, a sequence `shot_[0101-0103].exr`
// (126, 603-607).
let name = if name.is_empty() { default_footage_name(item, &f).unwrap_or_else(|| "Footage".to_string()) } else { name };
// One layer of a Photoshop file is `<layer>/<file>`, as EffectCraft's own Photoshop import
// names it (#3087).
let name = if name.is_empty() {
let file = default_footage_name(item, &f).unwrap_or_else(|| "Footage".to_string());
match &f.psd_layer {
Some(l) if !l.name.is_empty() => format!("{}/{file}", l.name),
_ => file,
}
} else {
name
};
if f.is_solid {
// A null object's placeholder solid is not an item here.
if aep_id.is_some_and(|i| null_only.contains(&i)) {
@@ -299,6 +310,7 @@ fn import_footage(project: &mut Project, cx: &mut Ctx<'_>, item: &AepItem<'_, '_
missing: !present,
placed_page,
sequence,
layer: source_layer(&f, &name, &mut cx.col),
..Footage::default()
};
// The native frame rate and the stored duration of sequences and clips (352, 603, 606).
@@ -338,6 +350,18 @@ fn is_placed_page_path(path: &str) -> bool {
matches!(ext.as_deref(), Some("pdf" | "ai"))
}
/// The Photoshop layer a footage item shows (#3087): at its own bounds when the item is as big
/// as they are (Retain Layer Sizes), else on the document. A layer without pixels (an adjustment
/// layer) keeps the merged image and is reported.
fn source_layer(f: &crate::Footage, name: &str, col: &mut Collector) -> Option<SourceLayer> {
let l = f.psd_layer.as_ref()?;
let Some(size) = l.size() else {
col.lose(LossCategory::UnsupportedItems, format!("footage '{name}': a Photoshop layer without pixels (an adjustment layer), the merged image used"));
return None;
};
Some(SourceLayer { index: l.index, name: l.name.clone(), layer_size: size == (f.width, f.height), ..SourceLayer::default() })
}
/// The name After Effects shows for a footage item that stores none: the file's name, or for a
/// sequence the name with the frame range (`shot_[0101-0103].exr`).
fn default_footage_name(item: &AepItem<'_, '_>, f: &crate::Footage) -> Option<String> {
+2 -1
View File
@@ -56,7 +56,8 @@ pub enum LossCategory {
/// Layers that could not be imported for another reason (unknown layer type, missing or
/// unsupported source, precomposition loop).
UnsupportedLayers,
/// Project items of a kind that is not imported.
/// Project items of a kind that is not imported, and items made from a Photoshop layer
/// without pixels (they show the merged image).
UnsupportedItems,
/// Data that could not be read (damaged headers, unreadable keyframes).
DamagedData,
+4 -4
View File
@@ -20,8 +20,8 @@ pub use container::{
GradientType, HueSaturationData, Interpolation, Item, ItemKind, Justification, KernRun, KeyEase, KeyLayout, KeyList, Keyframe, KeyframeKind, LIST, Layer,
LayerHeader, LayerRef, LayerType, LevelsData, LightType, Limits, ListHeader, Marker, Mask, MaskAtom, MaskMode, MatteMode, MeshData, NESTED_IDS,
Orientation, OrientationKey, ParagraphRun, ParamKind, ParamValue, PathVertex, PolystarType, Project, ProjectSettings, PropEntry, PropGroup, PropNode,
Property, Quality, Rational, Renderer, SequenceInfo, ShapeDetail, ShapeGroup, ShapeItem, ShapePath, SpatialKey, Stroke, StrokeCap, StrokeJoin, StyleRun,
TextBox, TextDocument, TextValue, TimeBase, TimeDisplay, TimeRemap, TimeRemapKey, TrackMatte, Tracked, Transform, ValueSource, Walk, WorkingSpace,
curve_lut, effect_map, identifier_channel_type, is_aepx, parse, parse_any, parse_text_dict, parse_with_limits, read_aepx, read_aepx_with_limits, style_map,
write, write_to,
Property, PsdLayer, Quality, Rational, Renderer, SequenceInfo, ShapeDetail, ShapeGroup, ShapeItem, ShapePath, SpatialKey, Stroke, StrokeCap, StrokeJoin,
StyleRun, TextBox, TextDocument, TextValue, TimeBase, TimeDisplay, TimeRemap, TimeRemapKey, TrackMatte, Tracked, Transform, ValueSource, Walk,
WorkingSpace, curve_lut, effect_map, identifier_channel_type, is_aepx, parse, parse_any, parse_text_dict, parse_with_limits, read_aepx,
read_aepx_with_limits, style_map, write, write_to,
};
@@ -720,6 +720,7 @@ fn extreme_values_in_the_batch_3_fields() {
file_size: 0,
audio_sample_rate: None,
is_sequence: true,
psd_layer: None,
};
for (num, den, rate) in [(3i64, 30i64, 30u32), (3, 0, 30), (-3, 30, 30), (i64::MAX, 1, u32::MAX), (i64::MIN, -1, u32::MAX), (1, i64::MAX, 1), (0, 1, 0)] {
let f = Footage { duration: Some(Rational { num, den }), native_frame_rate: rate, ..base.clone() };
+65
View File
@@ -842,3 +842,68 @@ fn a_default_state_colour_record_holds_the_value() {
assert_eq!(colour(&default_state), colour(&written));
assert_ne!(colour(&default_state), colour(&unchanged), "the recorded colour, not the default");
}
/// The project with its png item made one layer of a Photoshop file `art.psd`: the `opti` of
/// docs/aep-format.md 3.3 (tag `8BPS`, the layer index at 14, its bounds at 78-94, its name at
/// 344) and the item `size` big.
fn as_psd_layer(index: i32, bounds: [i32; 4], name: &str, size: (u32, u32)) -> Vec<u8> {
let mut opti = vec![0u8; 602];
opti[..4].copy_from_slice(b"8BPS");
opti[14..18].copy_from_slice(&index.to_be_bytes());
for (i, v) in bounds.iter().enumerate() {
opti[78 + 4 * i..82 + 4 * i].copy_from_slice(&v.to_le_bytes());
}
opti[344..344 + name.len()].copy_from_slice(name.as_bytes());
let alias = br#"{"fullpath":"C:\\x\\art.psd","platform":1,"target_is_folder":false}"#.to_vec();
mutate(&mut |c| {
if !c.is_list(b"Pin ") {
return;
}
let Body::List { children, .. } = &mut c.body else { return };
if !children.iter().any(|k| k.is(b"opti") && k.bytes().is_some_and(|d| d.get(..4) == Some(&b"png!"[..]))) {
return;
}
// No sequence naming between the alias and the `opti`.
children.retain(|k| k.is(b"sspc") || k.is(b"opti") || k.is_list(b"Als2"));
for k in children.iter_mut() {
if k.is(b"opti") {
k.body = Body::Data(opti.clone().into());
} else if k.is(b"sspc") {
set_data(k, 30, &size.0.to_be_bytes());
set_data(k, 34, &size.1.to_be_bytes());
}
}
visit(children, &mut |k| {
if k.is(b"alas") {
k.body = Body::Data(alias.clone().into());
}
});
})
}
/// #3087: an item made from one layer of a Photoshop file shows that layer, named
/// `<layer>/<file>`; it showed the whole document, named after the file. Item as big as the layer:
/// Retain Layer Sizes; as big as the document: the layer on the document.
#[test]
fn a_photoshop_layer_item_shows_its_layer() {
for (size, layer_size) in [((48, 15), true), ((1000, 1000), false)] {
let (p, r) = run(&as_psd_layer(7, [749, 800, 764, 848], "Tëst", size), true);
r.unwrap();
let (name, f) = p.items.values().find_map(|i| if let ItemKind::Footage(f) = &i.kind { Some((i.name.clone(), f.clone())) } else { None }).unwrap();
assert_eq!(name, "Tëst/art.psd");
let l = f.layer.expect("the layer");
assert_eq!((l.index, l.name.as_str(), l.layer_size, (f.width, f.height)), (7, "Tëst", layer_size, size));
}
}
/// #3087: a Photoshop layer without pixels (an adjustment layer) keeps the merged image and is
/// reported; the merged image itself (index -1) is no layer.
#[test]
fn a_photoshop_layer_without_pixels_keeps_the_merged_image() {
let (p, r) = run(&as_psd_layer(4, [0; 4], "Levels 1", (1000, 1000)), true);
let found = losses(&r.unwrap(), LossCategory::UnsupportedItems);
assert!(found.iter().any(|d| d.contains("'Levels 1/art.psd'") && d.contains("without pixels")), "{found:?}");
assert_eq!(the_footage(&p).layer, None);
let (p, _) = run(&as_psd_layer(-1, [0; 4], "", (1000, 1000)), true);
assert_eq!(the_footage(&p).layer, None);
}
+2 -1
View File
@@ -1,6 +1,6 @@
# After Effects project format (`.aep`)
> **Last reviewed:** 2026-11-05 · **Last updated:** 2026-11-07 · **Change:** Puppet (`ADBE FreePin3`, 3.7): the property tree from an external scripting listing, **?** until a byte sample exists (#3093); before that: footage: an image sequence without a filename prefix stores only the suffix `Utf8`, and one imported with Force alphabetical order stores its file list in `LIST StVc` / `StVS` (section 3, footage); before that: versions: `svap` is the version that wrote the file, `head` bytes 4 to 7 the version it is written for, and what a Save a Copy As 25.x / 24.x of After Effects 2026 changes (section 1, "Versions"); before that: the stretch numerator is signed (a negative one reverses the layer, 981-983); layer styles (3.12b, 950-980); before that: footage: `sspc@118` of an image sequence is its folder's modification time (860, read-back x1-x3); before that: text: paragraph `/24`, `/35` and style `/68` are neutral for 0 and 1 (732-734, edited in place); before that: keyframes: the ease of mask path keys (599, 700), the spatial speed unit of fractional values (597, 598, 730); before that: text: character and paragraph settings, manual kerning, vertical text, `/nil` is neutral (S17: 538-578, 595, 596); before that: cameras, lights and 3D rotations (S16: the auto Position of a two-node camera is where After Effects renders it, 537; camera and light defaults read back 255, 256; Orientation `otst/otda` 488), Timewarp (529-533), an expression on an effect parameter (534); before that: text: style slot /13 is superscript / subscript (526-528; every scripted text of the corpus is superscript), wrapping box text 517; before that: effects of batch 5 (409-450, 511-516: integer sliders, checkbox defaults in `pard`, arbitrary data of Hue/Saturation and Levels, the parameters of Invert, Fast Box Blur, Gaussian Blur, Drop Shadow, Glow; section 3.7); before that: shape items of batch 5 (451-484, 519: ellipse, fill and stroke opacity, fill rule, group transform, shape layer anchor, trim paths, rectangle roundness, polystar fields, gradient default, nested groups, the defaults of every item; section 3.9); before that: track mattes: mode at `ldta` byte 107 and the matte layer id at byte 160 (cases 402-408, section 3.1); before that: text: automatic leading is verified (400); Exposure (`ADBE Exposure2`, cases 389-397): group start / end parameter types 13 / 14, float slider valid minimum and slider range (section 3.7); before that: text: U+0003 is a paragraph break (386, 387); before that: a stored text Anchor Point is pixels (385, now V); before that: the XML form `.aepx` is read into the same chunk tree (section 1, "The XML form"); the `head` save counter is a `u32` at byte 16; text documents for the importer (stroke colour, automatic leading, the font of each run, the undecoded-slot check, the text-layer Anchor Point, the Source Text record); before that: third round of verified field layouts (explicit-default semantics, type-major bezier ease, exact time stretch and in / out points, Time Remapping state, comp pixel aspect / shutter phase / display start, Point default, Layer Control None, dropdown selection, one `mkif` per mask, polystar and radial gradient records, text styles, camera and light options, marker cue data, audio and image-sequence footage) added to the typed view of `effectcraft-aep` · **Target:** Adobe After Effects 2026
> **Last reviewed:** 2026-11-05 · **Last updated:** 2026-11-11 · **Change:** footage: the Photoshop layer an item shows (`opti` tag `8BPS`, section 3.3, issue 3087); before that: Puppet (`ADBE FreePin3`, 3.7): the property tree from an external scripting listing, **?** until a byte sample exists (#3093); before that: footage: an image sequence without a filename prefix stores only the suffix `Utf8`, and one imported with Force alphabetical order stores its file list in `LIST StVc` / `StVS` (section 3, footage); before that: versions: `svap` is the version that wrote the file, `head` bytes 4 to 7 the version it is written for, and what a Save a Copy As 25.x / 24.x of After Effects 2026 changes (section 1, "Versions"); before that: the stretch numerator is signed (a negative one reverses the layer, 981-983); layer styles (3.12b, 950-980); before that: footage: `sspc@118` of an image sequence is its folder's modification time (860, read-back x1-x3); before that: text: paragraph `/24`, `/35` and style `/68` are neutral for 0 and 1 (732-734, edited in place); before that: keyframes: the ease of mask path keys (599, 700), the spatial speed unit of fractional values (597, 598, 730); before that: text: character and paragraph settings, manual kerning, vertical text, `/nil` is neutral (S17: 538-578, 595, 596); before that: cameras, lights and 3D rotations (S16: the auto Position of a two-node camera is where After Effects renders it, 537; camera and light defaults read back 255, 256; Orientation `otst/otda` 488), Timewarp (529-533), an expression on an effect parameter (534); before that: text: style slot /13 is superscript / subscript (526-528; every scripted text of the corpus is superscript), wrapping box text 517; before that: effects of batch 5 (409-450, 511-516: integer sliders, checkbox defaults in `pard`, arbitrary data of Hue/Saturation and Levels, the parameters of Invert, Fast Box Blur, Gaussian Blur, Drop Shadow, Glow; section 3.7); before that: shape items of batch 5 (451-484, 519: ellipse, fill and stroke opacity, fill rule, group transform, shape layer anchor, trim paths, rectangle roundness, polystar fields, gradient default, nested groups, the defaults of every item; section 3.9); before that: track mattes: mode at `ldta` byte 107 and the matte layer id at byte 160 (cases 402-408, section 3.1); before that: text: automatic leading is verified (400); Exposure (`ADBE Exposure2`, cases 389-397): group start / end parameter types 13 / 14, float slider valid minimum and slider range (section 3.7); before that: text: U+0003 is a paragraph break (386, 387); before that: a stored text Anchor Point is pixels (385, now V); before that: the XML form `.aepx` is read into the same chunk tree (section 1, "The XML form"); the `head` save counter is a `u32` at byte 16; text documents for the importer (stroke colour, automatic leading, the font of each run, the undecoded-slot check, the text-layer Anchor Point, the Source Text record); before that: third round of verified field layouts (explicit-default semantics, type-major bezier ease, exact time stretch and in / out points, Time Remapping state, comp pixel aspect / shutter phase / display start, Point default, Layer Control None, dropdown selection, one `mkif` per mask, polystar and radial gradient records, text styles, camera and light options, marker cue data, audio and image-sequence footage) added to the typed view of `effectcraft-aep` · **Target:** Adobe After Effects 2026
What `crates/aep` ([`effectcraft-aep`](../crates/aep/README.md)) reads and writes, and the evidence
for each statement. Everything here was observed on project files we generated ourselves; no
@@ -263,6 +263,7 @@ property of `L` after 229 and 230). The corpus manifest calls the classic render
| footage size | `Pin /sspc` | 30, 34 | u32, u32 | width, height in pixels | 000 (1920 x 1080), 022 (100 x 100 null), 126 (8 x 8), 211, 212, 259 | V |
| footage name | `Pin /opti` | 26 (solid), 58 (file) | ASCII, NUL terminated | the item's own `Utf8` name is empty for footage | 000 (`L`), 022 (`Null 1`), 126 (`test8x8.png`), 211 (`S100`), 259 (`Blue`) | V |
| **solid colour** | `Pin /opti` | 10, 14, 18, 22 | 4 x f32 | ARGB, 0..1: red [1, 0, 0] is `1, 1, 0, 0`, blue [0, 0, 1] is `1, 0, 0, 1` | 000, 259 | V |
| **Photoshop layer** | `Pin /opti` (tag `8BPS`) | 14; 78, 82, 86, 90; 344 | i32 BE; 4 x i32 **LE**; UTF-8, NUL terminated | an item made from one layer of a `.psd`: the layer record's index in the file (bottom of the stack = 0; -1 is the merged image), the layer's bounds top, left, bottom, right in document pixels (all 0 for a layer without pixels, such as an adjustment layer) and the layer's name. The item's size (`sspc` 30 / 34) is the bounds' size with Retain Layer Sizes, else the document's. `Footage::psd_layer` | the project of issue 3087: ten layers, each index, bounds and name equal to the `.psd`'s layer record at that position (`Layer 1` `[9, -346, 1083, 1086]`, item 1432 x 1074; `test` `[749, 800, 764, 848]`, item 48 x 15) | V |
| file path | `Pin /Als2/alas` | key `fullpath` | JSON string | alias JSON of a file footage item | 126 (the fixture holds a neutral path) | V |
| precomp linkage | `Layr/ldta` | 40 | u32 | the layer's source id is the id of the precomp's item | 124 | V |
+3 -3
View File
File diff suppressed because one or more lines are too long