mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 15:24:42 +00:00
* craft-aep: a flat read-only tree index, borrowed text documents, fewer allocations The container part of storytold/effectcraft#3302: - `TreeIndex`: the chunk tree as one flat array in depth-first order, borrowed from the input, for readers that only look. Same chunks counted against the same limits and the same errors as `parse`. An `.aepx` is indexed by `index_aepx` into one buffer. The typed view reads through either tree (`Node`, `Root`, `Project::from_index`). - Chunk parser: each list of siblings is counted, then reserved once. `.aepx`: hex decoded into an output sized once, attribute values without an entity borrowed. `write` reserves the exact output size. - Text documents are parsed into a tree that borrows from the body; the style and paragraph defaults are built once. - Layer markers and Source Text are read where they are stored instead of searched for. - Container accessors that only tests used are removed. What the importer and exporter in EffectCraft reach across the crate boundary is public here: `Root`, `Draft`, `AepxDrafts`, `index_aepx`, `TreeIndex::from_drafts`, `Project::of`, `ProjectSettings::of`, `Node::marker_list`, `Root::file_version` and `saved_by_version`, `StyleRun::SAMPLED`. `TimeBase::NTSC` holds the rates as plain numerator and denominator instead of an EffectCraft type. * craft-aep: one decoder for an effect parameter definition (`Pard`) The container part of storytold/effectcraft#3319: the fields of a `pard` record (type code, name, default, valid range, slider range, popup item count) are read by one type, `Pard`, which `EffectParam` now reads through. It is public so that an exporter's tests can read back what they encode instead of keeping a second decoder. The rest of that PR (import and export bounds, loss reports) is in EffectCraft's importer and exporter. --------- Co-authored-by: vangel76 <vangel1976@googlemail.com>
469 lines
17 KiB
Rust
469 lines
17 KiB
Rust
//! The layer header (`ldta`) and the composition header (`cdta`, `PRin`).
|
|
//!
|
|
//! Every field here has a verified layout: the decoded value equals the value the generated corpus
|
|
//! was made with (see `docs/aep-format.md`, sections 4 and 5, for the evidence per field). Fields that
|
|
//! the corpus does not decide are not decoded; the raw bytes stay reachable through the chunk.
|
|
|
|
use crate::bytes::{array, be_i32, be_u16, be_u32, cstr, text, until_nul};
|
|
use crate::typed::{Comp, Item, Layer, TimeBase, utf8_child};
|
|
|
|
/// An exact fraction. Used for times (`seconds = num / den`) and for the pixel aspect ratio. The
|
|
/// fields hold the stored values unreduced; [`Rational::reduced`] normalises.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Rational {
|
|
pub num: i64,
|
|
pub den: i64,
|
|
}
|
|
|
|
impl Rational {
|
|
/// A fraction with a positive denominator, reduced; `None` for a zero denominator.
|
|
pub fn new(num: i64, den: i64) -> Option<Self> {
|
|
Self { num, den }.reduced()
|
|
}
|
|
|
|
/// The value as `f64`, `None` for a zero denominator.
|
|
pub fn to_f64(&self) -> Option<f64> {
|
|
(self.den != 0).then(|| self.num as f64 / self.den as f64)
|
|
}
|
|
|
|
/// The same value in lowest terms with a positive denominator, `None` for a zero denominator.
|
|
pub fn reduced(&self) -> Option<Self> {
|
|
if self.den == 0 {
|
|
return None;
|
|
}
|
|
let (mut a, mut b) = (i128::from(self.num).unsigned_abs(), i128::from(self.den).unsigned_abs());
|
|
let (mut x, mut y) = (a, b);
|
|
while y != 0 {
|
|
(x, y) = (y, x % y);
|
|
}
|
|
if x == 0 {
|
|
x = 1;
|
|
}
|
|
a /= x;
|
|
b /= x;
|
|
let negative = (self.num < 0) != (self.den < 0);
|
|
let num = i64::try_from(a).ok()?;
|
|
let den = i64::try_from(b).ok()?;
|
|
Some(Self { num: if negative { num.checked_neg()? } else { num }, den })
|
|
}
|
|
|
|
/// `self + other * factor`, exact, `None` on a zero denominator or overflow.
|
|
fn plus_scaled(&self, other: &Rational, factor: &Rational) -> Option<Rational> {
|
|
if self.den == 0 || other.den == 0 || factor.den == 0 {
|
|
return None;
|
|
}
|
|
// other * factor = (other.num * factor.num) / (other.den * factor.den)
|
|
let scaled_num = i128::from(other.num).checked_mul(i128::from(factor.num))?;
|
|
let scaled_den = i128::from(other.den).checked_mul(i128::from(factor.den))?;
|
|
let den = i128::from(self.den).checked_mul(scaled_den)?;
|
|
let num = i128::from(self.num).checked_mul(scaled_den)?.checked_add(scaled_num.checked_mul(i128::from(self.den))?)?;
|
|
Rational { num: i64::try_from(num).ok()?, den: i64::try_from(den).ok()? }.reduced()
|
|
}
|
|
}
|
|
|
|
/// The layer quality switch, `ldta` byte 5.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Quality {
|
|
/// `1`
|
|
Draft,
|
|
/// `2`
|
|
Best,
|
|
/// Any other value (`0` on cameras and lights; wireframe was not sampled).
|
|
Other(u8),
|
|
}
|
|
|
|
/// The blend mode, `ldta` byte 99. Six modes were sampled; any other mode is kept as its raw code.
|
|
/// The codes 2, 4, 5, 6 and 7 coincide with the numbers of the After Effects plug-in transfer
|
|
/// modes; Difference is 26.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum BlendMode {
|
|
/// `2`
|
|
Normal,
|
|
/// `4`
|
|
Add,
|
|
/// `5`
|
|
Multiply,
|
|
/// `6`
|
|
Screen,
|
|
/// `7`
|
|
Overlay,
|
|
/// `26`
|
|
Difference,
|
|
/// Any other value (not sampled; `0` on cameras and lights).
|
|
Other(u8),
|
|
}
|
|
|
|
impl BlendMode {
|
|
/// The mode for a code.
|
|
pub fn from_code(code: u8) -> Self {
|
|
match code {
|
|
2 => Self::Normal,
|
|
4 => Self::Add,
|
|
5 => Self::Multiply,
|
|
6 => Self::Screen,
|
|
7 => Self::Overlay,
|
|
26 => Self::Difference,
|
|
other => Self::Other(other),
|
|
}
|
|
}
|
|
|
|
/// The code as stored.
|
|
pub fn code(&self) -> u8 {
|
|
match self {
|
|
Self::Normal => 2,
|
|
Self::Add => 4,
|
|
Self::Multiply => 5,
|
|
Self::Screen => 6,
|
|
Self::Overlay => 7,
|
|
Self::Difference => 26,
|
|
Self::Other(code) => *code,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The light type, `ldta` `u32` at byte 136 of a light layer (0 on every other layer).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum LightType {
|
|
/// `0` (701)
|
|
Parallel,
|
|
/// `1`
|
|
Spot,
|
|
/// `2`
|
|
Point,
|
|
/// `3` (702; a light created as an environment light by script without the Advanced 3D
|
|
/// renderer is stored as an ambient light too, 704)
|
|
Ambient,
|
|
/// Any other value (an environment light was not sampled).
|
|
Other(u32),
|
|
}
|
|
|
|
/// The layer type, `ldta` `u32` at byte 128.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum LayerType {
|
|
/// `0`: solid, footage, precomp or null object (an AV layer).
|
|
Av,
|
|
/// `1`
|
|
Light,
|
|
/// `2`
|
|
Camera,
|
|
/// `3`
|
|
Text,
|
|
/// `4`
|
|
Shape,
|
|
/// Any other value.
|
|
Other(u32),
|
|
}
|
|
|
|
/// The decoded fields of a layer's `ldta` chunk (164 bytes; the fields below need the first 140).
|
|
///
|
|
/// Times are stored as a numerator and a denominator (the denominator is the composition's
|
|
/// timescale, 1 in a precomp layer). The in and out points are stored **relative to the start**:
|
|
/// the absolute in point is `start + in * stretch`, see [`LayerHeader::in_point`].
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct LayerHeader {
|
|
/// The layer id (unique in the project); parent links use it.
|
|
pub id: u32,
|
|
pub quality: Quality,
|
|
/// The numerator of the time stretch (signed `i32` at byte 8). The stretch is the exact ratio
|
|
/// `stretch / stretch_den`: `1/1` = 100 %, `2/1` = 200 %, `1/2` = 50 %, `3/2` = 150 %, `-1/1` =
|
|
/// -100 % (a time-reversed layer, 981), `-1/2` = -50 % (983), see
|
|
/// [`LayerHeader::stretch_ratio`].
|
|
pub stretch: i32,
|
|
/// The denominator of the time stretch (`u32` at byte 108; 1 in every file before 50 % was
|
|
/// sampled).
|
|
pub stretch_den: u32,
|
|
/// The layer start time, as stored.
|
|
pub start: Rational,
|
|
/// The in point relative to the start, as stored.
|
|
pub in_relative: Rational,
|
|
/// The out point relative to the start, as stored.
|
|
pub out_relative: Rational,
|
|
/// Bytes 37, 38 and 39 of `ldta`: the switch bits (see the `is_*` methods).
|
|
pub flag_bytes: [u8; 3],
|
|
/// The id of the source item (`ldta` byte 40), `None` for the values `0` (shape, text, camera)
|
|
/// and `0xffff_ffff` (light).
|
|
pub source_id: Option<u32>,
|
|
/// The label colour index (`ldta` byte 61: 1 red, 5, 8 blue, ...).
|
|
pub label: u8,
|
|
/// The copy of the layer name at byte 64 (31 characters at most; the layer's `Utf8` child holds
|
|
/// the full name).
|
|
pub name_copy: String,
|
|
pub blend_mode: BlendMode,
|
|
pub layer_type: LayerType,
|
|
/// The id of the parent layer, `None` when there is none.
|
|
pub parent_id: Option<u32>,
|
|
/// The `u32` at byte 136 (the light type of a light layer, 0 elsewhere), see
|
|
/// [`LayerHeader::light_type`].
|
|
pub light_type_code: u32,
|
|
}
|
|
|
|
impl LayerHeader {
|
|
/// Decodes an `ldta` body; `None` when it is shorter than 140 bytes.
|
|
pub fn parse(ldta: &[u8]) -> Option<Self> {
|
|
let time = |at: usize| Some(Rational { num: i64::from(be_i32(ldta, at)?), den: i64::from(be_u32(ldta, at.checked_add(4)?)?) });
|
|
let source = be_u32(ldta, 40)?;
|
|
let parent = be_u32(ldta, 132)?;
|
|
Some(Self {
|
|
id: be_u32(ldta, 0)?,
|
|
quality: match *ldta.get(5)? {
|
|
1 => Quality::Draft,
|
|
2 => Quality::Best,
|
|
other => Quality::Other(other),
|
|
},
|
|
stretch: be_i32(ldta, 8)?,
|
|
stretch_den: be_u32(ldta, 108)?,
|
|
start: time(12)?,
|
|
in_relative: time(20)?,
|
|
out_relative: time(28)?,
|
|
flag_bytes: array::<3>(ldta, 37)?,
|
|
source_id: (source != 0 && source != u32::MAX).then_some(source),
|
|
label: *ldta.get(61)?,
|
|
name_copy: text(until_nul(ldta.get(64..96)?)),
|
|
blend_mode: BlendMode::from_code(*ldta.get(99)?),
|
|
layer_type: match be_u32(ldta, 128)? {
|
|
0 => LayerType::Av,
|
|
1 => LayerType::Light,
|
|
2 => LayerType::Camera,
|
|
3 => LayerType::Text,
|
|
4 => LayerType::Shape,
|
|
other => LayerType::Other(other),
|
|
},
|
|
parent_id: (parent != 0).then_some(parent),
|
|
light_type_code: be_u32(ldta, 136)?,
|
|
})
|
|
}
|
|
|
|
fn flag(&self, byte: usize, mask: u8) -> bool {
|
|
self.flag_bytes.get(byte).is_some_and(|b| b & mask != 0)
|
|
}
|
|
|
|
/// Byte 37 bit `0x02`: guide layer.
|
|
pub fn is_guide(&self) -> bool {
|
|
self.flag(0, 0x02)
|
|
}
|
|
|
|
/// Byte 38 bit `0x02`: adjustment layer.
|
|
pub fn is_adjustment(&self) -> bool {
|
|
self.flag(1, 0x02)
|
|
}
|
|
|
|
/// Byte 38 bit `0x04`: the 3D layer switch.
|
|
pub fn is_three_d(&self) -> bool {
|
|
self.flag(1, 0x04)
|
|
}
|
|
|
|
/// Byte 38 bit `0x40`: the layer orients itself towards its point of interest. Set on two-node
|
|
/// cameras (byte 38 is `0x44`) and on all lights, clear on a one-node camera (`0x04`).
|
|
pub fn is_auto_oriented(&self) -> bool {
|
|
self.flag(1, 0x40)
|
|
}
|
|
|
|
/// Byte 38 bit `0x08`: solo.
|
|
pub fn is_solo(&self) -> bool {
|
|
self.flag(1, 0x08)
|
|
}
|
|
|
|
/// Byte 38 bit `0x80`: the layer was created as a null object.
|
|
pub fn is_null_object(&self) -> bool {
|
|
self.flag(1, 0x80)
|
|
}
|
|
|
|
/// Byte 39 bit `0x01`: the video switch (the layer's eye icon; `enabled` in the scripting API).
|
|
pub fn is_video_enabled(&self) -> bool {
|
|
self.flag(2, 0x01)
|
|
}
|
|
|
|
/// The same as [`LayerHeader::is_video_enabled`].
|
|
pub fn is_enabled(&self) -> bool {
|
|
self.is_video_enabled()
|
|
}
|
|
|
|
/// Byte 39 bit `0x02`: the audio switch (`audioEnabled`).
|
|
pub fn is_audio_enabled(&self) -> bool {
|
|
self.flag(2, 0x02)
|
|
}
|
|
|
|
/// Byte 39 bit `0x04`: the effects switch (`effectsActive`).
|
|
pub fn is_effects_enabled(&self) -> bool {
|
|
self.flag(2, 0x04)
|
|
}
|
|
|
|
/// Byte 39 bit `0x08`: the motion blur switch.
|
|
pub fn is_motion_blur(&self) -> bool {
|
|
self.flag(2, 0x08)
|
|
}
|
|
|
|
/// Byte 39 bit `0x20`: locked.
|
|
pub fn is_locked(&self) -> bool {
|
|
self.flag(2, 0x20)
|
|
}
|
|
|
|
/// Byte 39 bit `0x40`: shy.
|
|
pub fn is_shy(&self) -> bool {
|
|
self.flag(2, 0x40)
|
|
}
|
|
|
|
/// Byte 39 bit `0x80`: collapse transformations / continuously rasterize. Shape and text
|
|
/// layers have it set from creation.
|
|
pub fn collapses_transformations(&self) -> bool {
|
|
self.flag(2, 0x80)
|
|
}
|
|
|
|
/// The light type of a light layer: parallel 0, spot 1, point 2, ambient 3 (`None` for any
|
|
/// other layer type).
|
|
pub fn light_type(&self) -> Option<LightType> {
|
|
(self.layer_type == LayerType::Light).then_some(match self.light_type_code {
|
|
0 => LightType::Parallel,
|
|
1 => LightType::Spot,
|
|
2 => LightType::Point,
|
|
3 => LightType::Ambient,
|
|
other => LightType::Other(other),
|
|
})
|
|
}
|
|
|
|
/// The time stretch as an exact reduced ratio (`stretch / stretch_den`: 50 % is `1/2`, 150 %
|
|
/// is `3/2`); `None` for a zero denominator.
|
|
pub fn stretch_ratio(&self) -> Option<Rational> {
|
|
Rational::new(i64::from(self.stretch), i64::from(self.stretch_den))
|
|
}
|
|
|
|
/// The absolute in point, `start + in * stretch`, exact and reduced.
|
|
pub fn in_point(&self) -> Option<Rational> {
|
|
self.start.plus_scaled(&self.in_relative, &self.stretch_ratio()?)
|
|
}
|
|
|
|
/// The absolute out point, `start + out * stretch`, exact and reduced. For a footage layer the
|
|
/// stored denominator is the footage time base (the sample rate of an audio layer, the frame
|
|
/// rate of an image sequence), not the composition timescale.
|
|
pub fn out_point(&self) -> Option<Rational> {
|
|
self.start.plus_scaled(&self.out_relative, &self.stretch_ratio()?)
|
|
}
|
|
}
|
|
|
|
impl<'f, 'a> Layer<'f, 'a> {
|
|
/// The layer id (`ldta` byte 0).
|
|
pub fn id(&self) -> Option<u32> {
|
|
be_u32(self.chunk.child(b"ldta")?.bytes()?, 0)
|
|
}
|
|
|
|
/// The decoded `ldta`, see [`LayerHeader`].
|
|
pub fn header(&self) -> Option<LayerHeader> {
|
|
LayerHeader::parse(self.chunk.child(b"ldta")?.bytes()?)
|
|
}
|
|
|
|
/// The video switch (byte 39 bit `0x01`), `enabled` in the scripting API.
|
|
pub fn is_enabled(&self) -> Option<bool> {
|
|
self.header().map(|h| h.is_video_enabled())
|
|
}
|
|
|
|
/// The audio switch (byte 39 bit `0x02`), `audioEnabled`.
|
|
pub fn is_audio_enabled(&self) -> Option<bool> {
|
|
self.header().map(|h| h.is_audio_enabled())
|
|
}
|
|
|
|
/// The effects switch (byte 39 bit `0x04`), `effectsActive`.
|
|
pub fn is_effects_enabled(&self) -> Option<bool> {
|
|
self.header().map(|h| h.is_effects_enabled())
|
|
}
|
|
}
|
|
|
|
/// The renderer of a composition (`LIST PRin` / `prin`).
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Renderer {
|
|
/// The match name (`ADBE Escher` for the Classic 3D renderer in the base project,
|
|
/// `ADBE Calder` for Advanced 3D).
|
|
pub match_name: String,
|
|
/// The display name (`Classic 3D`, `Advanced 3D`).
|
|
pub display_name: String,
|
|
}
|
|
|
|
/// The decoded fields of a composition's `cdta` chunk (204 bytes; the fields below need 176).
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct CompHeader {
|
|
/// Frame duration (`u32` at byte 4) and timescale (`u32` at byte 8).
|
|
pub time_base: TimeBase,
|
|
/// The frame rate as 16.16 fixed point (`u32` at byte 156).
|
|
pub frame_rate_fixed: u32,
|
|
/// The composition duration in time units.
|
|
pub duration_units: u32,
|
|
/// The work area start in time units.
|
|
pub work_area_start_units: u32,
|
|
/// The work area end (start + duration) in time units; `None` when it is the whole
|
|
/// composition (stored as `0xffff_ffff`).
|
|
pub work_area_end_units: Option<u32>,
|
|
/// The background colour as R, G, B (bytes 52 to 54), 8 bits per channel: 0.5 is stored as 128
|
|
/// and reads back as 128/255.
|
|
pub background: [u8; 3],
|
|
/// The shutter angle in degrees.
|
|
pub shutter_angle: u32,
|
|
pub width: u16,
|
|
pub height: u16,
|
|
/// The pixel aspect ratio as stored: numerator and denominator.
|
|
pub pixel_aspect: Rational,
|
|
/// The composition's motion blur switch.
|
|
pub motion_blur: bool,
|
|
/// The shutter phase in degrees (`i32` at byte 180, 0 by default, -90 in the sample); `None`
|
|
/// when the chunk ends before byte 184.
|
|
pub shutter_phase: Option<i32>,
|
|
/// The display start time (the comp start timecode) as `num / den` seconds: `u32` at byte 164
|
|
/// over `u32` at byte 168 (0/1 by default; display start frame 100 at 25 fps is 102400/25600).
|
|
/// `None` when the chunk ends before byte 172.
|
|
pub display_start: Option<Rational>,
|
|
}
|
|
|
|
impl CompHeader {
|
|
/// Decodes a `cdta` body; `None` when it is shorter than 176 bytes.
|
|
pub fn parse(cdta: &[u8]) -> Option<Self> {
|
|
let work_end = be_u32(cdta, 36)?;
|
|
Some(Self {
|
|
time_base: TimeBase { frame_duration: be_u32(cdta, 4)?, timescale: be_u32(cdta, 8)? },
|
|
frame_rate_fixed: be_u32(cdta, 156)?,
|
|
duration_units: be_u32(cdta, 44)?,
|
|
work_area_start_units: be_u32(cdta, 28)?,
|
|
work_area_end_units: (work_end != u32::MAX).then_some(work_end),
|
|
background: array::<3>(cdta, 52)?,
|
|
shutter_angle: be_u32(cdta, 172)?,
|
|
width: be_u16(cdta, 140)?,
|
|
height: be_u16(cdta, 142)?,
|
|
pixel_aspect: Rational { num: i64::from(be_u32(cdta, 144)?), den: i64::from(be_u32(cdta, 148)?) },
|
|
motion_blur: cdta.get(139)? & 0x08 != 0,
|
|
shutter_phase: be_i32(cdta, 180),
|
|
display_start: be_u32(cdta, 164).zip(be_u32(cdta, 168)).map(|(num, den)| Rational { num: i64::from(num), den: i64::from(den) }),
|
|
})
|
|
}
|
|
|
|
/// Frames per second from the time base (`timescale / frame_duration`).
|
|
pub fn frame_rate(&self) -> Option<f64> {
|
|
self.time_base.frame_rate()
|
|
}
|
|
|
|
/// The duration in seconds.
|
|
pub fn duration_seconds(&self) -> Option<f64> {
|
|
self.time_base.seconds(i32::try_from(self.duration_units).ok()?)
|
|
}
|
|
}
|
|
|
|
impl<'f, 'a> Comp<'f, 'a> {
|
|
/// The decoded `cdta`, see [`CompHeader`].
|
|
pub fn header(&self) -> Option<CompHeader> {
|
|
CompHeader::parse(self.item.child(b"cdta")?.bytes()?)
|
|
}
|
|
|
|
/// The composition name (the item's `Utf8` child).
|
|
pub fn name(&self) -> Option<String> {
|
|
utf8_child(self.item).filter(|n| !n.is_empty())
|
|
}
|
|
|
|
/// The item this composition data belongs to.
|
|
pub fn item(&self) -> Item<'f, 'a> {
|
|
Item { chunk: self.item }
|
|
}
|
|
|
|
/// The renderer (`LIST PRin` / `prin`: match name at byte 4, display name at byte 52, 32
|
|
/// bytes each).
|
|
pub fn renderer(&self) -> Option<Renderer> {
|
|
let prin = self.item.child_list(b"PRin")?.child(b"prin")?.bytes()?;
|
|
Some(Renderer { match_name: cstr(prin, 4, 32)?, display_name: cstr(prin, 52, 32)? })
|
|
}
|
|
}
|