mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-14 22:33:09 +00:00
Merge remote-tracking branch 'origin/aep-export-dedup-3278' into aep-audit-3300
This commit is contained in:
commit
a7456fac62
14 files changed
+599
-350
No files matched your search
Generated
+1
@@ -1453,6 +1453,7 @@ dependencies = [
|
||||
"effectcraft-keyframe",
|
||||
"effectcraft-project",
|
||||
"effectcraft-time",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
|
||||
@@ -15,6 +15,7 @@ effectcraft-color = { workspace = true }
|
||||
effectcraft-keyframe = { workspace = true }
|
||||
effectcraft-project = { workspace = true }
|
||||
effectcraft-effects = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ mod footage;
|
||||
mod fx_defs;
|
||||
mod fxsynth;
|
||||
pub(crate) mod icc;
|
||||
mod keys;
|
||||
mod markers;
|
||||
mod masks;
|
||||
mod plan;
|
||||
|
||||
@@ -6,15 +6,19 @@
|
||||
//! (read-backs t-no<id>, t-zcdta, u-zsspc, u-noclrs, u-noftgi, u-nofrec, u-nocdrp).
|
||||
|
||||
use crate::Chunk;
|
||||
use crate::export::synth::{tdmn, tdsn};
|
||||
use crate::export::synth::{GROUP_END, tdmn, tdsn};
|
||||
use crate::export::tree::Fixed;
|
||||
|
||||
/// A zero buffer of `len` bytes with `fields` (offset, bytes) written into it.
|
||||
/// A zero buffer of `len` bytes with `fields` (offset, bytes) written into it. The layouts are
|
||||
/// fixed in code ([`Fixed`]).
|
||||
pub(crate) fn bytes(len: usize, fields: &[(usize, &[u8])]) -> Vec<u8> {
|
||||
let mut b = vec![0u8; len];
|
||||
layout(len, fields).finish(|| format!("base::bytes ({len} bytes, fields at {:?})", fields.iter().map(|(at, v)| (*at, v.len())).collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
fn layout(len: usize, fields: &[(usize, &[u8])]) -> Fixed {
|
||||
let mut b = Fixed::zeroed(len);
|
||||
for (at, v) in fields {
|
||||
if let Some(dst) = b.get_mut(*at..at.saturating_add(v.len())) {
|
||||
dst.copy_from_slice(v);
|
||||
}
|
||||
b.put(*at, v);
|
||||
}
|
||||
b
|
||||
}
|
||||
@@ -52,12 +56,8 @@ pub(crate) fn project_chunks() -> Vec<Chunk<'static>> {
|
||||
// colour depth (2: 32 bits); the save noise after the code page is left zero.
|
||||
let nhed = bytes(32, &[(7, &[5]), (9, &[1]), (12, &[0x1e, 16, 2, 2]), (18, &UTF8_CODE_PAGE)]);
|
||||
let nnhd = bytes(40, &[(7, &[5]), (9, &[1]), (15, &[0x1e]), (19, &[16, 2]), (24, &[2]), (26, &UTF8_CODE_PAGE)]);
|
||||
let mut lhd3 = bytes(52, &[(0, &0x00d0_0bee_u32.to_be_bytes()), (12, &1u32.to_be_bytes()), (16, &RQ_ITEM_LEN.to_be_bytes())]);
|
||||
for at in [20, 24, 28] {
|
||||
if let Some(dst) = lhd3.get_mut(at..at + 4) {
|
||||
dst.copy_from_slice(&1u32.to_be_bytes());
|
||||
}
|
||||
}
|
||||
let one = 1u32.to_be_bytes();
|
||||
let lhd3 = bytes(52, &[(0, &0x00d0_0bee_u32.to_be_bytes()), (12, &one), (16, &RQ_ITEM_LEN.to_be_bytes()), (20, &one), (24, &one), (28, &one)]);
|
||||
let wsnm: Vec<u8> = "Default".encode_utf16().flat_map(u16::to_be_bytes).collect();
|
||||
vec![
|
||||
Chunk::data(*b"svap", VERSION.to_vec()),
|
||||
@@ -157,30 +157,17 @@ fn view_layer(ty: &[u8; 4], name: &str, label: u8, kind: u8) -> Vec<Chunk<'stati
|
||||
let markers = kind == 4;
|
||||
let mut name_copy = name.as_bytes().to_vec();
|
||||
name_copy.truncate(31);
|
||||
let mut ldta = bytes(
|
||||
164,
|
||||
&[
|
||||
(8, &1u32.to_be_bytes()),
|
||||
(16, &600u32.to_be_bytes()),
|
||||
(24, &25_600u32.to_be_bytes()),
|
||||
(28, &VIEW_OUT.to_be_bytes()),
|
||||
(32, &25_600u32.to_be_bytes()),
|
||||
(61, &[label]),
|
||||
(64, &name_copy),
|
||||
(108, &1u32.to_be_bytes()),
|
||||
(128, &u32::from(kind).to_be_bytes()),
|
||||
],
|
||||
);
|
||||
// Byte 38: 3D and auto-orient on the cameras; byte 39: the video switch (and on the markers
|
||||
// layer also effects, locked and collapse); quality Draft and blend mode Normal on the
|
||||
// markers layer.
|
||||
let flags: &[(usize, u8)] = if markers { &[(5, 2), (39, 0x87), (99, 2)] } else { &[(38, 0x44), (39, 1)] };
|
||||
for (at, v) in flags {
|
||||
if let Some(b) = ldta.get_mut(*at) {
|
||||
*b = *v;
|
||||
}
|
||||
}
|
||||
let root = vec![Chunk::data(*b"tdsb", 1u32.to_be_bytes().to_vec()), tdsn(""), tdmn("ADBE Group End")];
|
||||
let flags: &[(usize, &[u8])] = if markers { &[(5, &[2]), (39, &[0x87]), (99, &[2])] } else { &[(38, &[0x44]), (39, &[1])] };
|
||||
let (one, scale, kind_word) = (1u32.to_be_bytes(), 25_600u32.to_be_bytes(), u32::from(kind).to_be_bytes());
|
||||
let (base, out, label) = (600u32.to_be_bytes(), VIEW_OUT.to_be_bytes(), [label]);
|
||||
let mut fields: Vec<(usize, &[u8])> =
|
||||
vec![(8, &one), (16, &base), (24, &scale), (28, &out), (32, &scale), (61, &label), (64, &name_copy), (108, &one), (128, &kind_word)];
|
||||
fields.extend_from_slice(flags);
|
||||
let ldta = bytes(164, &fields);
|
||||
let root = vec![Chunk::data(*b"tdsb", 1u32.to_be_bytes().to_vec()), tdsn(""), tdmn(GROUP_END)];
|
||||
let layer = Chunk::list(*ty, vec![Chunk::data(*b"ldta", ldta), utf8(name), Chunk::list(*b"tdgp", root), guides()]);
|
||||
let mut out = vec![layer, Chunk::list(*b"Ewst", Vec::new())];
|
||||
out.extend(crate::export::skeleton::f_records());
|
||||
@@ -293,6 +280,15 @@ pub(crate) fn solid_item() -> Chunk<'static> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_field_out_of_range_is_an_error() {
|
||||
let ok = layout(4, &[(0, &[1]), (3, &[2])]);
|
||||
assert!(ok.error().is_none());
|
||||
assert_eq!(ok.finish(String::new), [1, 0, 0, 2]);
|
||||
assert!(layout(4, &[(3, &[1, 2])]).error().is_some());
|
||||
assert!(layout(4, &[(usize::MAX, &[1])]).error().is_some());
|
||||
}
|
||||
|
||||
/// The chunks After Effects refuses a file without, or crashes on (aep-export.md, "What After
|
||||
/// Effects needs").
|
||||
#[test]
|
||||
|
||||
@@ -13,7 +13,8 @@ use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::{Layer, LightKind};
|
||||
|
||||
use crate::export::report::{Collector, LossCategory};
|
||||
use crate::export::tree::{R, child_data, kids_mut, list_mut, put_f64, tpl};
|
||||
use crate::export::synth::{GROUP_END, is_tdmn, tdmn};
|
||||
use crate::export::tree::{R, child_data, kids_mut, list_mut, put_f64, split_group, tpl};
|
||||
|
||||
/// A camera's options in the file's units (pixels, percent, a switch).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -183,50 +184,24 @@ const LIGHT_ORDER: &[&str] = &[
|
||||
"ADBE Casts Shadows",
|
||||
];
|
||||
|
||||
pub(crate) fn tdmn_chunk(name: &str) -> Chunk<'static> {
|
||||
let mut b = vec![0u8; 40];
|
||||
for (i, c) in name.bytes().take(39).enumerate() {
|
||||
b[i] = c;
|
||||
}
|
||||
Chunk::data(*b"tdmn", b)
|
||||
}
|
||||
|
||||
fn tdmn_name(c: &Chunk<'_>) -> Option<String> {
|
||||
let b = c.bytes()?;
|
||||
let end = b.iter().position(|x| *x == 0).unwrap_or(b.len());
|
||||
std::str::from_utf8(b.get(..end)?).ok().map(str::to_string)
|
||||
}
|
||||
|
||||
/// Sets (or inserts) the records of an options group (`tdmn` named `group`) of a layer.
|
||||
fn put_group(layr: &mut Chunk<'static>, group: &str, order: &[&str], set: Vec<(&str, Chunk<'static>)>, remove: &[&str]) -> R<()> {
|
||||
let root = list_mut(layr, b"tdgp")?;
|
||||
let kids = kids_mut(root)?;
|
||||
let at = kids.iter().position(|c| c.is(b"tdmn") && tdmn_name(c).as_deref() == Some(group)).ok_or_else(|| tpl("options group missing"))?;
|
||||
let at = kids.iter().position(|c| is_tdmn(c, group)).ok_or_else(|| tpl("options group missing"))?;
|
||||
let g = kids.get_mut(at + 1).ok_or_else(|| tpl("options group without a list"))?;
|
||||
let gk = kids_mut(g)?;
|
||||
let first = gk.iter().position(|c| c.is(b"tdmn")).unwrap_or(gk.len());
|
||||
let header: Vec<Chunk<'static>> = gk.drain(..first).collect();
|
||||
let mut pairs: Vec<(String, Vec<Chunk<'static>>)> = Vec::new();
|
||||
let mut it = std::mem::take(gk).into_iter();
|
||||
while let Some(m) = it.next() {
|
||||
let name = tdmn_name(&m).unwrap_or_default();
|
||||
if name == "ADBE Group End" {
|
||||
pairs.push((name, vec![m]));
|
||||
continue;
|
||||
}
|
||||
let Some(node) = it.next() else { break };
|
||||
// An atom group (Light Env Atom) carries its own Group End inside the list.
|
||||
pairs.push((name, vec![m, node]));
|
||||
}
|
||||
// An atom group (Light Env Atom) carries its own Group End inside its list.
|
||||
let (header, mut pairs) = split_group(gk)?;
|
||||
pairs.retain(|(n, _)| !remove.contains(&n.as_str()));
|
||||
for (name, rec) in set {
|
||||
let entry = vec![tdmn_chunk(name), rec];
|
||||
let entry = vec![tdmn(name), rec];
|
||||
match pairs.iter_mut().find(|(n, _)| n == name) {
|
||||
Some(slot) => slot.1 = entry,
|
||||
None => pairs.push((name.to_string(), entry)),
|
||||
}
|
||||
}
|
||||
let rank = |n: &str| if n == "ADBE Group End" { usize::MAX } else { order.iter().position(|g| *g == n).unwrap_or(order.len()) };
|
||||
let rank = |n: &str| if n == GROUP_END { usize::MAX } else { order.iter().position(|g| *g == n).unwrap_or(order.len()) };
|
||||
pairs.sort_by_key(|(n, _)| rank(n));
|
||||
gk.extend(header);
|
||||
gk.extend(pairs.into_iter().flat_map(|(_, v)| v));
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
use crate::Chunk;
|
||||
use crate::effect_map::{Conv, by_match_name};
|
||||
use crate::export::fx_defs::{BUILT_IN, FxDef, PDef};
|
||||
use crate::export::synth::{KeyTemplate, NO_NAME, RecSpec, class, tdmn, tdsn};
|
||||
use crate::export::synth::{GROUP_END, KeyTemplate, NO_NAME, RecSpec, class, tdmn, tdsn, utf8_in};
|
||||
use crate::export::tree::Fixed;
|
||||
use crate::{CURVE_IDENTITY, CurvesData, MeshData};
|
||||
|
||||
/// The layer a prototype's layer parameters name when their default is the owning layer; the
|
||||
@@ -37,12 +38,6 @@ pub(crate) fn value_spec(code: u8, range: Option<(f64, f64)>) -> Option<RecSpec>
|
||||
})
|
||||
}
|
||||
|
||||
fn put(b: &mut [u8], at: usize, bytes: &[u8]) {
|
||||
if let Some(dst) = b.get_mut(at..at + bytes.len()) {
|
||||
dst.copy_from_slice(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
fn fixed(v: f64) -> [u8; 4] {
|
||||
((v * 65536.0).round().clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32).to_be_bytes()
|
||||
}
|
||||
@@ -53,83 +48,87 @@ fn fixed(v: f64) -> [u8; 4] {
|
||||
/// display 124; fixed and integer: 124 / 128 / 132 / 136, default 140, display 144). Every other
|
||||
/// byte is zero (the switches at 48 are not needed: s12-e2-nofl).
|
||||
pub(crate) fn pard(p: &PDef, popup_items: Option<u16>) -> Vec<u8> {
|
||||
let mut b = vec![0u8; 148];
|
||||
put(&mut b, 12, &u32::from(p.code).to_be_bytes());
|
||||
pard_layout(p, popup_items).finish(|| format!("pard (type {})", p.code))
|
||||
}
|
||||
|
||||
fn pard_layout(p: &PDef, popup_items: Option<u16>) -> Fixed {
|
||||
let mut b = Fixed::zeroed(148);
|
||||
b.put(12, &u32::from(p.code).to_be_bytes());
|
||||
let d = |i: usize| p.default.get(i).copied().unwrap_or(0.0);
|
||||
// After Effects takes the default of a parameter without a value record from here (s8-e1:
|
||||
// the copies below are the ones it reads).
|
||||
match p.code {
|
||||
10 => {
|
||||
put(&mut b, 56, &d(0).to_be_bytes());
|
||||
b.put(56, &d(0).to_be_bytes());
|
||||
// The default again as `f32` at 120, the display word (decimals) at 124.
|
||||
put(&mut b, 120, &(d(0) as f32).to_be_bytes());
|
||||
put(&mut b, 124, &p.display.to_be_bytes());
|
||||
b.put(120, &(d(0) as f32).to_be_bytes());
|
||||
b.put(124, &p.display.to_be_bytes());
|
||||
}
|
||||
2 => put(&mut b, 56, &fixed(d(0))),
|
||||
2 => b.put(56, &fixed(d(0))),
|
||||
// An angle: the default again at 60.
|
||||
3 => {
|
||||
put(&mut b, 56, &fixed(d(0)));
|
||||
put(&mut b, 60, &fixed(d(0)));
|
||||
b.put(56, &fixed(d(0)));
|
||||
b.put(60, &fixed(d(0)));
|
||||
}
|
||||
1 => put(&mut b, 56, &(d(0).round() as i32).to_be_bytes()),
|
||||
1 => b.put(56, &(d(0).round() as i32).to_be_bytes()),
|
||||
// A checkbox: the default again as a byte at 60.
|
||||
4 => {
|
||||
put(&mut b, 56, &(d(0).round().max(0.0) as u32).to_be_bytes());
|
||||
put(&mut b, 60, &[u8::from(d(0) != 0.0)]);
|
||||
b.put(56, &(d(0).round().max(0.0) as u32).to_be_bytes());
|
||||
b.put(60, &[u8::from(d(0) != 0.0)]);
|
||||
}
|
||||
// A popup: the default again as `u16` at 62 (the item count is at 60).
|
||||
7 => {
|
||||
let sel = d(0).round().clamp(0.0, f64::from(u16::MAX));
|
||||
put(&mut b, 56, &(sel as u32).to_be_bytes());
|
||||
put(&mut b, 62, &(sel as u16).to_be_bytes());
|
||||
b.put(56, &(sel as u32).to_be_bytes());
|
||||
b.put(62, &(sel as u16).to_be_bytes());
|
||||
}
|
||||
6 => {
|
||||
// Fractions of the layer at 56 / 60 and the same in percent at 68 / 72.
|
||||
put(&mut b, 56, &fixed(d(0)));
|
||||
put(&mut b, 60, &fixed(d(1)));
|
||||
put(&mut b, 68, &fixed(d(0) * 100.0));
|
||||
put(&mut b, 72, &fixed(d(1) * 100.0));
|
||||
b.put(56, &fixed(d(0)));
|
||||
b.put(60, &fixed(d(1)));
|
||||
b.put(68, &fixed(d(0) * 100.0));
|
||||
b.put(72, &fixed(d(1) * 100.0));
|
||||
}
|
||||
5 => {
|
||||
// ARGB at 56 and the same at 60.
|
||||
let c: Vec<u8> = (0..4).map(|i| d(i).round().clamp(0.0, 255.0) as u8).collect();
|
||||
put(&mut b, 56, &c);
|
||||
put(&mut b, 60, &c);
|
||||
b.put(56, &c);
|
||||
b.put(60, &c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if p.code == 7 {
|
||||
let n = popup_items.unwrap_or_else(|| u16::try_from(p.display).unwrap_or(0));
|
||||
put(&mut b, 60, &n.to_be_bytes());
|
||||
b.put(60, &n.to_be_bytes());
|
||||
}
|
||||
match p.code {
|
||||
10 => {
|
||||
if let Some(m) = p.valid_min {
|
||||
put(&mut b, 104, &(m as f32).to_be_bytes());
|
||||
b.put(104, &(m as f32).to_be_bytes());
|
||||
}
|
||||
if let Some(m) = p.valid_max {
|
||||
put(&mut b, 108, &(m as f32).to_be_bytes());
|
||||
b.put(108, &(m as f32).to_be_bytes());
|
||||
}
|
||||
if let Some((lo, hi)) = p.range {
|
||||
put(&mut b, 112, &(lo as f32).to_be_bytes());
|
||||
put(&mut b, 116, &(hi as f32).to_be_bytes());
|
||||
b.put(112, &(lo as f32).to_be_bytes());
|
||||
b.put(116, &(hi as f32).to_be_bytes());
|
||||
}
|
||||
}
|
||||
1 | 2 => {
|
||||
let enc = |v: f64| if p.code == 2 { fixed(v) } else { (v.round() as i32).to_be_bytes() };
|
||||
if let Some(m) = p.valid_min {
|
||||
put(&mut b, 124, &enc(m));
|
||||
b.put(124, &enc(m));
|
||||
}
|
||||
if let Some(m) = p.valid_max {
|
||||
put(&mut b, 128, &enc(m));
|
||||
b.put(128, &enc(m));
|
||||
}
|
||||
if let Some((lo, hi)) = p.range {
|
||||
put(&mut b, 132, &enc(lo));
|
||||
put(&mut b, 136, &enc(hi));
|
||||
b.put(132, &enc(lo));
|
||||
b.put(136, &enc(hi));
|
||||
}
|
||||
// The default again at 140, the display word (decimals, percent) at 144.
|
||||
put(&mut b, 140, &enc(d(0)));
|
||||
put(&mut b, 144, &p.display.to_be_bytes());
|
||||
b.put(140, &enc(d(0)));
|
||||
b.put(144, &p.display.to_be_bytes());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -224,7 +223,7 @@ fn data_record(data: Vec<u8>) -> [Chunk<'static>; 2] {
|
||||
/// group (records only for layer parameters, groups, buttons and arbitrary data, the built-in
|
||||
/// group) and `pgui` (zero).
|
||||
pub(crate) fn default_sspc(fx: &FxDef) -> Chunk<'static> {
|
||||
let fnam = Chunk { id: *b"fnam", body: crate::Body::Chunks(vec![Chunk::data(*b"Utf8", fx.name.as_bytes().to_vec())]), pad: None };
|
||||
let fnam = utf8_in(*b"fnam", fx.name);
|
||||
let count = u32::try_from(fx.params.len()).unwrap_or(u32::MAX);
|
||||
let mut part = vec![Chunk::data(*b"parn", count.to_be_bytes().to_vec())];
|
||||
let mut values = vec![Chunk::data(*b"tdsb", 1u32.to_be_bytes().to_vec()), tdsn(NO_NAME)];
|
||||
@@ -235,11 +234,11 @@ pub(crate) fn default_sspc(fx: &FxDef) -> Chunk<'static> {
|
||||
if matches!(p.code, 4 | 7 | 15) {
|
||||
// Checkboxes, popups and buttons have a `pdnm` (their label or items); its text may
|
||||
// be empty.
|
||||
part.push(Chunk { id: *b"pdnm", body: crate::Body::Chunks(vec![Chunk::data(*b"Utf8", Vec::new())]), pad: None });
|
||||
part.push(utf8_in(*b"pdnm", ""));
|
||||
}
|
||||
if p.id == BUILT_IN {
|
||||
values.push(tdmn(&record));
|
||||
values.push(Chunk::list(*b"tdgp", vec![Chunk::data(*b"tdsb", 1u32.to_be_bytes().to_vec()), tdsn(NO_NAME), tdmn("ADBE Group End")]));
|
||||
values.push(Chunk::list(*b"tdgp", vec![Chunk::data(*b"tdsb", 1u32.to_be_bytes().to_vec()), tdsn(NO_NAME), tdmn(GROUP_END)]));
|
||||
continue;
|
||||
}
|
||||
match p.code {
|
||||
@@ -265,6 +264,24 @@ pub(crate) fn default_sspc(fx: &FxDef) -> Chunk<'static> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
values.push(tdmn("ADBE Group End"));
|
||||
values.push(tdmn(GROUP_END));
|
||||
Chunk::list(*b"sspc", vec![fnam, Chunk::list(*b"parT", part), Chunk::list(*b"tdgp", values), Chunk::data(*b"pgui", vec![0u8; 16])])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::export::fx_defs::FX_DEFS;
|
||||
|
||||
/// The `pard` of every parameter of the effect table fits its 148 bytes.
|
||||
#[test]
|
||||
fn every_parameter_definition_fits() {
|
||||
for fx in FX_DEFS {
|
||||
for (i, p) in fx.params.iter().enumerate() {
|
||||
let b = pard_layout(p, popup_items(fx, i));
|
||||
assert!(b.error().is_none(), "{} {}", fx.match_name, i);
|
||||
assert_eq!(b.finish(String::new).len(), 148);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//! Key lists shared by the exporters: the times of a property's keys in the file's units, the
|
||||
//! spatial key record, and the rewrite of a key list (`LIST list`: `lhd3` + `ldat`, aep-format.md
|
||||
//! 3.5).
|
||||
|
||||
use effectcraft_keyframe::Keyframe;
|
||||
|
||||
use crate::Chunk;
|
||||
use crate::export::plan::KeyPlan;
|
||||
use crate::export::report::{Collector, LossCategory};
|
||||
use crate::export::timebase::TimeBase;
|
||||
use crate::export::tree::{R, child_data, kids_mut, list_mut, put, put_f64, put_u8, put_u16, put_u32, tpl};
|
||||
|
||||
/// The keys of a property at their times in the file's units, in time order. A key that does not
|
||||
/// fall on the time base is reported as moved; keys that end up at the same time are merged (the
|
||||
/// first one is kept) and reported once. `noun` names the key in the report ("key", "path key").
|
||||
pub(crate) fn timed_keys<'k>(keys: &'k [Keyframe], tb: &TimeBase, what: &str, noun: &str, col: &mut Collector) -> Vec<(i64, &'k Keyframe)> {
|
||||
let mut timed: Vec<(i64, &Keyframe)> = Vec::with_capacity(keys.len());
|
||||
for k in keys {
|
||||
let (u, inexact) = tb.units(k.time);
|
||||
if inexact {
|
||||
col.lose(LossCategory::RoundedTimes, format!("{what}: {noun} at {:.4} s moved to the time base", k.time.seconds()));
|
||||
}
|
||||
timed.push((u, k));
|
||||
}
|
||||
timed.sort_by_key(|(u, _)| *u);
|
||||
let before = timed.len();
|
||||
timed.dedup_by_key(|(u, _)| *u);
|
||||
if timed.len() != before {
|
||||
col.lose(LossCategory::ApproximatedKeyframes, format!("{what}: {noun}s at the same time were merged"));
|
||||
}
|
||||
timed
|
||||
}
|
||||
|
||||
/// A key of a point (flags `0x0007`, `u32@8` the automatic tangent switch; the spatial layout of
|
||||
/// a 2D Position, 853) or a colour (flags `0x0001`, 854): the temporal ease of a point at 24..55,
|
||||
/// then the `n` values at 56 and, for a point, the spatial in and out tangents after them.
|
||||
pub(crate) fn spatial_record(k: &KeyPlan, n: usize, point: bool, size: usize) -> R<Vec<u8>> {
|
||||
let mut r = vec![0u8; size];
|
||||
put(&mut r, 0, &k.time.to_be_bytes())?;
|
||||
put_u8(&mut r, 4, k.in_interp)?;
|
||||
put_u8(&mut r, 5, k.out_interp)?;
|
||||
let base: u16 = if point { 0x0007 } else { 0x0001 };
|
||||
put_u16(&mut r, 6, base | (u16::from(k.label) << 6))?;
|
||||
if point {
|
||||
put_u32(&mut r, 8, u32::from(k.spatial_auto))?;
|
||||
let (i, o) = (k.in_ease.first().copied().unwrap_or_default(), k.out_ease.first().copied().unwrap_or_default());
|
||||
put_f64(&mut r, 24, i.speed)?;
|
||||
put_f64(&mut r, 32, i.influence)?;
|
||||
put_f64(&mut r, 40, o.speed)?;
|
||||
put_f64(&mut r, 48, o.influence)?;
|
||||
}
|
||||
for j in 0..n {
|
||||
put_f64(&mut r, 56 + 8 * j, k.value.get(j).copied().unwrap_or(0.0))?;
|
||||
if point {
|
||||
put_f64(&mut r, 56 + 8 * n + 8 * j, k.spatial_in.get(j).copied().unwrap_or(0.0))?;
|
||||
put_f64(&mut r, 56 + 16 * n + 8 * j, k.spatial_out.get(j).copied().unwrap_or(0.0))?;
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Fills a key list (`LIST list`) with `count` records of `size` bytes: the count words of `lhd3`
|
||||
/// and `ldat` (added when the list has none).
|
||||
pub(crate) fn fill_key_list(list: &mut Chunk<'static>, records: Vec<u8>, count: usize, size: usize) -> R<()> {
|
||||
let n = u32::try_from(count).map_err(|_| tpl("too many keys"))?;
|
||||
let size = u32::try_from(size).map_err(|_| tpl("record size"))?;
|
||||
crate::export::synth::put_key_counts(child_data(list, b"lhd3")?, n, size)?;
|
||||
let kids = kids_mut(list)?;
|
||||
match kids.iter_mut().find(|c| c.is(b"ldat")) {
|
||||
Some(c) => *c = Chunk::data(*b"ldat", records),
|
||||
None => kids.push(Chunk::data(*b"ldat", records)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rewrites the key list of a keyed property record (`LIST tdbs`), see [`fill_key_list`].
|
||||
pub(crate) fn set_key_list(tdbs: &mut Chunk<'static>, records: Vec<u8>, count: usize, size: usize) -> R<()> {
|
||||
fill_key_list(list_mut(tdbs, b"list")?, records, count, size)
|
||||
}
|
||||
|
||||
/// Puts the key list `list` in the place of the static value (`cdat`) of a property record.
|
||||
pub(crate) fn replace_cdat(tdbs: &mut Chunk<'static>, list: Chunk<'static>) -> R<()> {
|
||||
let slot = kids_mut(tdbs)?.iter_mut().find(|c| c.is(b"cdat")).ok_or_else(|| tpl("record without cdat"))?;
|
||||
*slot = list;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::export::synth::key_list;
|
||||
|
||||
#[test]
|
||||
fn keys_are_timed_sorted_merged_and_reported() {
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_time::{TICKS_PER_SECOND, Tick};
|
||||
let tb = TimeBase { frame_duration: 1024, timescale: 25600 };
|
||||
// Out of order; 1/3 s is not on the time base; two keys meet at 1 s (the first one kept).
|
||||
let keys = [
|
||||
Keyframe::new(Tick(TICKS_PER_SECOND), Value::Scalar(1.0)),
|
||||
Keyframe::new(Tick(TICKS_PER_SECOND / 3), Value::Scalar(2.0)),
|
||||
Keyframe::new(Tick(0), Value::Scalar(3.0)),
|
||||
Keyframe::new(Tick(TICKS_PER_SECOND), Value::Scalar(4.0)),
|
||||
];
|
||||
let mut col = Collector::default();
|
||||
let timed = timed_keys(&keys, &tb, "L", "path key", &mut col);
|
||||
let got: Vec<(i64, f64)> = timed.iter().map(|(u, k)| (*u, if let Value::Scalar(v) = k.value { v } else { f64::NAN })).collect();
|
||||
assert_eq!(got, [(0, 3.0), (8533, 2.0), (25600, 1.0)]);
|
||||
// The report lists its categories in a fixed order; one merge report for the pair.
|
||||
let report = col.finish();
|
||||
let lines: Vec<(LossCategory, &str)> = report.losses.iter().flat_map(|l| l.details.iter().map(move |d| (l.category, d.as_str()))).collect();
|
||||
assert_eq!(
|
||||
lines,
|
||||
[
|
||||
(LossCategory::ApproximatedKeyframes, "L: path keys at the same time were merged"),
|
||||
(LossCategory::RoundedTimes, "L: path key at 0.3333 s moved to the time base"),
|
||||
]
|
||||
);
|
||||
// No keys, no report.
|
||||
let mut col = Collector::default();
|
||||
assert!(timed_keys(&[], &tb, "L", "key", &mut col).is_empty());
|
||||
assert!(col.finish().losses.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_key_list_is_filled_and_replaces_the_value() {
|
||||
let mut list = Chunk::list(*b"list", vec![Chunk::data(*b"lhd3", vec![0; 52])]);
|
||||
fill_key_list(&mut list, vec![7; 96], 2, 48).unwrap();
|
||||
let h = list.child(b"lhd3").and_then(|c| c.bytes()).unwrap();
|
||||
assert_eq!((h[11], h[19]), (2, 48));
|
||||
assert_eq!(list.child(b"ldat").and_then(|c| c.bytes()).map(<[u8]>::len), Some(96));
|
||||
// A second fill replaces the records instead of adding a second `ldat`.
|
||||
fill_key_list(&mut list, vec![1; 48], 1, 48).unwrap();
|
||||
assert_eq!(list.children().iter().filter(|c| c.is(b"ldat")).count(), 1);
|
||||
|
||||
let mut rec = Chunk::list(*b"tdbs", vec![Chunk::data(*b"tdb4", vec![0; 124]), Chunk::data(*b"cdat", vec![0; 8])]);
|
||||
replace_cdat(&mut rec, key_list(48, 0, Vec::new())).unwrap();
|
||||
assert!(rec.child(b"cdat").is_none() && rec.child_list(b"list").is_some());
|
||||
// A record without a value is an error, not a quiet no-op.
|
||||
assert!(replace_cdat(&mut rec, key_list(48, 0, Vec::new())).is_err());
|
||||
set_key_list(&mut rec, vec![0; 48], 1, 48).unwrap();
|
||||
assert!(set_key_list(&mut Chunk::list(*b"tdbs", Vec::new()), Vec::new(), 0, 48).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_spatial_record_holds_the_point_layout() {
|
||||
let k = KeyPlan {
|
||||
time: 5,
|
||||
value: vec![1.0, 2.0],
|
||||
in_interp: 2,
|
||||
out_interp: 1,
|
||||
in_ease: Vec::new(),
|
||||
out_ease: Vec::new(),
|
||||
spatial_in: [3.0; 3],
|
||||
spatial_out: [4.0; 3],
|
||||
spatial_auto: true,
|
||||
roving: false,
|
||||
label: 1,
|
||||
};
|
||||
let r = spatial_record(&k, 2, true, 56 + 24 * 2).unwrap();
|
||||
assert_eq!(&r[..12], &[0, 0, 0, 5, 2, 1, 0, 0x47, 0, 0, 0, 1]);
|
||||
assert_eq!(&r[64..72], &2.0f64.to_be_bytes());
|
||||
assert_eq!(&r[96..104], &4.0f64.to_be_bytes());
|
||||
// A colour has no tangents and flags 0x0001.
|
||||
let c = spatial_record(&k, 2, false, 56 + 24 * 2).unwrap();
|
||||
assert_eq!((c[7], c[11]), (0x41, 0));
|
||||
assert!(c[72..].iter().all(|b| *b == 0));
|
||||
// A record shorter than its layout is an error.
|
||||
assert!(spatial_record(&k, 2, true, 64).is_err());
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,11 @@ use crate::Chunk;
|
||||
use effectcraft_keyframe::{Ease, Interp, ShapePath, Value};
|
||||
use effectcraft_project::{FeatherFalloff, GroupKind, Layer, MaskMode, MaskMotionBlur, PropGroup};
|
||||
|
||||
use crate::export::keys::{set_key_list, timed_keys};
|
||||
use crate::export::plan::{CompPlan, EaseSlot, KeyPlan, LIMIT, cut_name};
|
||||
use crate::export::report::{Collector, LossCategory};
|
||||
use crate::export::tree::{R, child_data, child_mut, kids_mut, list_mut, put_f32, put_f64, put_i32, put_u8, put_u16, put_u32, tpl};
|
||||
use crate::export::synth::GROUP_END;
|
||||
use crate::export::tree::{Fixed, R, child_data, child_mut, kids_mut, list_mut, put_f32, put_f64, put_i32, put_u8, put_u16, put_u32, split_group, tpl};
|
||||
|
||||
/// The most vertices one mask path may have (as the importer).
|
||||
pub(crate) const MAX_VERTICES: usize = 65_536;
|
||||
@@ -179,20 +181,7 @@ fn plan_mask(g: &PropGroup, size: (u32, u32), cp: &CompPlan, what: &str, col: &m
|
||||
}
|
||||
// The path and its keys.
|
||||
let mut path_keys: Vec<PathKeyPlan> = Vec::new();
|
||||
let mut keys: Vec<(i64, &effectcraft_keyframe::Keyframe)> = Vec::new();
|
||||
for k in &path_prop.keys {
|
||||
let (u, inexact) = cp.tb.units(k.time);
|
||||
if inexact {
|
||||
col.lose(LossCategory::RoundedTimes, format!("{what}: path key at {:.4} s moved to the time base", k.time.seconds()));
|
||||
}
|
||||
keys.push((u, k));
|
||||
}
|
||||
keys.sort_by_key(|(u, _)| *u);
|
||||
let before = keys.len();
|
||||
keys.dedup_by_key(|(u, _)| *u);
|
||||
if keys.len() != before {
|
||||
col.lose(LossCategory::ApproximatedKeyframes, format!("{what}: path keys at the same time were merged"));
|
||||
}
|
||||
let keys = timed_keys(&path_prop.keys, &cp.tb, what, "path key", col);
|
||||
let side = |i: Interp| -> u8 {
|
||||
match i {
|
||||
Interp::Linear => 1,
|
||||
@@ -288,20 +277,7 @@ fn plan_mask(g: &PropGroup, size: (u32, u32), cp: &CompPlan, what: &str, col: &m
|
||||
|
||||
/// Feather keys (plain template, two components, speed in pixels per second).
|
||||
fn plan_feather_keys(src: &[effectcraft_keyframe::Keyframe], cp: &CompPlan, what: &str, col: &mut Collector) -> Vec<KeyPlan> {
|
||||
let mut keys: Vec<(i64, &effectcraft_keyframe::Keyframe)> = Vec::with_capacity(src.len());
|
||||
for k in src {
|
||||
let (u, inexact) = cp.tb.units(k.time);
|
||||
if inexact {
|
||||
col.lose(LossCategory::RoundedTimes, format!("{what}: feather key at {:.4} s moved to the time base", k.time.seconds()));
|
||||
}
|
||||
keys.push((u, k));
|
||||
}
|
||||
keys.sort_by_key(|(u, _)| *u);
|
||||
let before = keys.len();
|
||||
keys.dedup_by_key(|(u, _)| *u);
|
||||
if keys.len() != before {
|
||||
col.lose(LossCategory::ApproximatedKeyframes, format!("{what}: feather keys at the same time were merged"));
|
||||
}
|
||||
let keys = timed_keys(src, &cp.tb, what, "feather key", col);
|
||||
let mut out = Vec::with_capacity(keys.len());
|
||||
let beyond = std::cell::Cell::new(false);
|
||||
for (u, k) in &keys {
|
||||
@@ -381,11 +357,6 @@ pub(crate) struct MaskProtos {
|
||||
pub keyed_feather: Chunk<'static>,
|
||||
}
|
||||
|
||||
pub(crate) fn tdmn_name(c: &Chunk<'_>) -> Option<String> {
|
||||
let d = c.bytes()?;
|
||||
Some(d.iter().take_while(|b| **b != 0).map(|b| char::from(*b)).collect())
|
||||
}
|
||||
|
||||
/// The limit of a mask's feather and expansion (`tduM` 32 000, 091).
|
||||
const MASK_LIMIT: f64 = 32_000.0;
|
||||
|
||||
@@ -394,9 +365,24 @@ const MASK_LIMIT: f64 = 32_000.0;
|
||||
/// s13-m1), byte 3 `0x01` (`0x09` once the path is open), the box at 4..19 (written per shape),
|
||||
/// byte 20 1.
|
||||
pub(crate) fn shph() -> Vec<u8> {
|
||||
let mut b = vec![0u8; 24];
|
||||
let _ = crate::export::tree::put(&mut b, 0, &[0xb3, 0xde, 0x02, 0x01]);
|
||||
let _ = put_u8(&mut b, 20, 1);
|
||||
shph_layout().finish(|| "shph".to_string())
|
||||
}
|
||||
|
||||
fn shph_layout() -> Fixed {
|
||||
let mut b = Fixed::zeroed(24);
|
||||
b.put(0, &[0xb3, 0xde, 0x02, 0x01]);
|
||||
b.put(20, &[1]);
|
||||
b
|
||||
}
|
||||
|
||||
/// The two prototype records of a keyed path (093): 64 bytes each, flags `0x0001`, the `f64` at
|
||||
/// 16 1 in the first (the rest is zero: its last bytes were uninitialised).
|
||||
fn path_key_records() -> Fixed {
|
||||
let mut b = Fixed::zeroed(128);
|
||||
for at in [6, 64 + 6] {
|
||||
b.put(at, &0x0001u16.to_be_bytes());
|
||||
}
|
||||
b.put(16, &1.0f64.to_be_bytes());
|
||||
b
|
||||
}
|
||||
|
||||
@@ -413,15 +399,7 @@ impl MaskProtos {
|
||||
let path_rec = |keyed: bool| {
|
||||
let mut kids = vec![state(), tdsn(NO_NAME), Chunk::data(*b"tdb4", tdb4(&path, keyed, 25_600))];
|
||||
if keyed {
|
||||
// Path keys are 64-byte records: flags `0x0001`, the `f64` at 16 1 on the first
|
||||
// key (093; the rest of the record is zero: its last bytes were uninitialised).
|
||||
let mut first = vec![0u8; 64];
|
||||
let mut later = vec![0u8; 64];
|
||||
for r in [&mut first, &mut later] {
|
||||
let _ = put_u16(r, 6, 0x0001);
|
||||
}
|
||||
let _ = put_f64(&mut first, 16, 1.0);
|
||||
kids.push(key_list(64, 2, [first, later].concat()));
|
||||
kids.push(key_list(64, 2, path_key_records().finish(|| "the keyed path records".to_string())));
|
||||
} else {
|
||||
kids.push(Chunk::data(*b"cdat", vec![0u8; 4]));
|
||||
}
|
||||
@@ -444,7 +422,7 @@ impl MaskProtos {
|
||||
tdbs(&opacity, false, 25_600),
|
||||
tdmn("ADBE Mask Offset"),
|
||||
tdbs(&offset, false, 25_600),
|
||||
tdmn("ADBE Group End"),
|
||||
tdmn(GROUP_END),
|
||||
],
|
||||
);
|
||||
MaskProtos {
|
||||
@@ -453,7 +431,7 @@ impl MaskProtos {
|
||||
atom_name: tdmn("ADBE Mask Atom"),
|
||||
mkif: Chunk::data(*b"mkif", vec![0u8; 48]),
|
||||
atom,
|
||||
group_end: tdmn("ADBE Group End"),
|
||||
group_end: tdmn(GROUP_END),
|
||||
keyed_path: path_rec(true),
|
||||
keyed_feather: tdbs(&feather, true, 25_600),
|
||||
}
|
||||
@@ -485,27 +463,21 @@ fn build_atom(t: &MaskProtos, m: &MaskPlan, size: (u32, u32)) -> R<Chunk<'static
|
||||
let tdsn = child_mut(&mut atom, b"tdsn")?;
|
||||
crate::export::tree::set_utf8(tdsn, &m.name)?;
|
||||
}
|
||||
let kids = kids_mut(&mut atom)?;
|
||||
let first = kids.iter().position(|c| c.is(b"tdmn")).ok_or_else(|| tpl("empty mask atom"))?;
|
||||
let header: Vec<Chunk<'static>> = kids.drain(..first).collect();
|
||||
let mut pairs: Vec<(String, [Chunk<'static>; 2])> = Vec::new();
|
||||
let mut end: Option<Chunk<'static>> = None;
|
||||
let mut it = std::mem::take(kids).into_iter();
|
||||
while let Some(c) = it.next() {
|
||||
let name = tdmn_name(&c).unwrap_or_default();
|
||||
if name == "ADBE Group End" {
|
||||
end = Some(c);
|
||||
continue;
|
||||
}
|
||||
let node = it.next().ok_or_else(|| tpl("mask match name without a node"))?;
|
||||
pairs.push((name, [c, node]));
|
||||
}
|
||||
let end = end.ok_or_else(|| tpl("mask atom without its end"))?;
|
||||
let mut out = header;
|
||||
for (name, [tdmn, mut node]) in pairs {
|
||||
let (mut out, entries) = split_group(kids_mut(&mut atom)?)?;
|
||||
let mut has_shape = false;
|
||||
for (name, entry) in entries {
|
||||
// The closing Group End (no node) is kept as it is.
|
||||
let [tdmn, mut node]: [Chunk<'static>; 2] = match entry.try_into() {
|
||||
Ok(pair) => pair,
|
||||
Err(end) => {
|
||||
out.extend(end);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let keep = match name.as_str() {
|
||||
"ADBE Mask Shape" => {
|
||||
build_shape(t, &mut node, m, size)?;
|
||||
has_shape = true;
|
||||
true
|
||||
}
|
||||
"ADBE Mask Feather" => {
|
||||
@@ -540,7 +512,10 @@ fn build_atom(t: &MaskProtos, m: &MaskPlan, size: (u32, u32)) -> R<Chunk<'static
|
||||
out.push(node);
|
||||
}
|
||||
}
|
||||
out.push(end);
|
||||
// A prototype without the path would drop every mask's shape.
|
||||
if !has_shape {
|
||||
return Err(tpl("mask atom without its path"));
|
||||
}
|
||||
*kids_mut(&mut atom)? = out;
|
||||
Ok(atom)
|
||||
}
|
||||
@@ -554,22 +529,6 @@ fn set_cdat(tdbs: &mut Chunk<'static>, values: &[f64]) -> R<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rewrites a key list (`lhd3` count words and `ldat`).
|
||||
fn set_key_list(tdbs: &mut Chunk<'static>, records: Vec<u8>, count: usize, size: usize) -> R<()> {
|
||||
let list = list_mut(tdbs, b"list")?;
|
||||
let n = u32::try_from(count).map_err(|_| tpl("too many keys"))?;
|
||||
{
|
||||
let lhd3 = child_data(list, b"lhd3")?;
|
||||
crate::export::synth::put_key_counts(lhd3, n, u32::try_from(size).map_err(|_| tpl("record size"))?)?;
|
||||
}
|
||||
let ld = kids_mut(list)?;
|
||||
match ld.iter_mut().find(|c| c.is(b"ldat")) {
|
||||
Some(c) => *c = Chunk::data(*b"ldat", records),
|
||||
None => ld.push(Chunk::data(*b"ldat", records)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keyed_feather(t: &MaskProtos, keys: &[KeyPlan]) -> R<Chunk<'static>> {
|
||||
let mut tdbs = t.keyed_feather.clone();
|
||||
// The plain template with two components (208): 8 + 40 * 2 bytes.
|
||||
@@ -669,3 +628,35 @@ pub(crate) fn build_shap(proto: &Chunk<'static>, s: &ShapePlan) -> R<Chunk<'stat
|
||||
}
|
||||
Ok(shap)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::export::synth::is_tdmn;
|
||||
|
||||
fn mask() -> MaskPlan {
|
||||
let shape = ShapePlan { bbox: [0.0, 0.0, 1.0, 1.0], open: false, points: vec![[0.0, 0.0]; 3] };
|
||||
MaskPlan {
|
||||
name: "M".into(),
|
||||
mode: 1,
|
||||
inverted: false,
|
||||
shape,
|
||||
path_keys: Vec::new(),
|
||||
feather: None,
|
||||
feather_keys: Vec::new(),
|
||||
opacity: None,
|
||||
expansion: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_atom_without_its_path_is_an_error() {
|
||||
let mut t = MaskProtos::build();
|
||||
assert!(build_atom(&t, &mask(), (100, 100)).is_ok());
|
||||
// Without the Mask Shape pair the path of every mask would be dropped quietly.
|
||||
let kids = kids_mut(&mut t.atom).unwrap();
|
||||
let at = kids.iter().position(|c| is_tdmn(c, "ADBE Mask Shape")).unwrap();
|
||||
kids.drain(at..at + 2);
|
||||
assert!(build_atom(&t, &mask(), (100, 100)).is_err());
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,12 @@ use crate::Chunk;
|
||||
use effectcraft_keyframe::{Gradient, Interp, Keyframe, Value};
|
||||
use effectcraft_project::{Node, PropGroup, Property};
|
||||
|
||||
use crate::export::keys::{fill_key_list, replace_cdat, spatial_record, timed_keys};
|
||||
use crate::export::masks::ShapePlan;
|
||||
use crate::export::plan::{CompPlan, EaseSlot, KeyPlan};
|
||||
use crate::export::report::{Collector, LossCategory};
|
||||
use crate::export::tree::{R, child_data, kids_mut, list_mut, put, put_f64, put_u8, put_u16, put_u32, set_utf8, tpl};
|
||||
use crate::export::synth::{GROUP_END, is_tdmn, tdmn};
|
||||
use crate::export::tree::{R, child_data, kids_mut, list_mut, put_f64, put_u8, set_utf8, tpl};
|
||||
|
||||
/// The deepest group nesting written.
|
||||
const MAX_DEPTH: usize = 16;
|
||||
@@ -192,7 +194,7 @@ impl ShapeProtos {
|
||||
put_f64(child_data(&mut node, b"tdum")?, 0, lo)?;
|
||||
put_f64(child_data(&mut node, b"tduM")?, 0, hi)?;
|
||||
}
|
||||
Ok([crate::export::camlight::tdmn_chunk(name), node])
|
||||
Ok([tdmn(name), node])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,20 +269,7 @@ pub(crate) struct ShCx<'a> {
|
||||
|
||||
/// The keys of `p` in the file's units.
|
||||
fn plan_keys(p: &Property, conv: Conv, layout: Layout, what: &str, cx: &mut ShCx<'_>) -> Vec<KeyPlan> {
|
||||
let mut timed: Vec<(i64, &Keyframe)> = Vec::with_capacity(p.keys.len());
|
||||
for k in &p.keys {
|
||||
let (u, inexact) = cx.cp.tb.units(k.time);
|
||||
if inexact {
|
||||
cx.col.lose(LossCategory::RoundedTimes, format!("{what}: key at {:.4} s moved to the time base", k.time.seconds()));
|
||||
}
|
||||
timed.push((u, k));
|
||||
}
|
||||
timed.sort_by_key(|(u, _)| *u);
|
||||
let before = timed.len();
|
||||
timed.dedup_by_key(|(u, _)| *u);
|
||||
if timed.len() != before {
|
||||
cx.col.lose(LossCategory::ApproximatedKeyframes, format!("{what}: keys at the same time were merged"));
|
||||
}
|
||||
let timed = timed_keys(&p.keys, &cx.cp.tb, what, "key", cx.col);
|
||||
let all: Vec<Keyframe> = timed.iter().map(|(_, k)| (*k).clone()).collect();
|
||||
let mut out = Vec::with_capacity(timed.len());
|
||||
let beyond = std::cell::Cell::new(false);
|
||||
@@ -618,38 +607,6 @@ pub(crate) fn plan_contents(contents: &PropGroup, what: &str, depth: usize, cx:
|
||||
out
|
||||
}
|
||||
|
||||
fn tdmn_name(c: &Chunk<'_>) -> Option<String> {
|
||||
let b = c.bytes()?;
|
||||
let end = b.iter().position(|x| *x == 0).unwrap_or(b.len());
|
||||
std::str::from_utf8(b.get(..end)?).ok().map(str::to_string)
|
||||
}
|
||||
|
||||
/// A key of a point (the spatial layout of a 2D Position, 853) or a colour (854).
|
||||
fn spatial_key(k: &KeyPlan, n: usize, point: bool, size: usize) -> R<Vec<u8>> {
|
||||
let mut r = vec![0u8; size];
|
||||
put(&mut r, 0, &k.time.to_be_bytes())?;
|
||||
put_u8(&mut r, 4, k.in_interp)?;
|
||||
put_u8(&mut r, 5, k.out_interp)?;
|
||||
let base: u16 = if point { 0x0007 } else { 0x0001 };
|
||||
put_u16(&mut r, 6, base | (u16::from(k.label) << 6))?;
|
||||
if point {
|
||||
put_u32(&mut r, 8, u32::from(k.spatial_auto))?;
|
||||
let (i, o) = (k.in_ease.first().copied().unwrap_or_default(), k.out_ease.first().copied().unwrap_or_default());
|
||||
put_f64(&mut r, 24, i.speed)?;
|
||||
put_f64(&mut r, 32, i.influence)?;
|
||||
put_f64(&mut r, 40, o.speed)?;
|
||||
put_f64(&mut r, 48, o.influence)?;
|
||||
}
|
||||
for j in 0..n {
|
||||
put_f64(&mut r, 56 + 8 * j, k.value.get(j).copied().unwrap_or(0.0))?;
|
||||
if point {
|
||||
put_f64(&mut r, 56 + 8 * n + 8 * j, k.spatial_in.get(j).copied().unwrap_or(0.0))?;
|
||||
put_f64(&mut r, 56 + 16 * n + 8 * j, k.spatial_out.get(j).copied().unwrap_or(0.0))?;
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// The value record of `name` holding `v`.
|
||||
fn value_record(t: &ShapeProtos, name: &'static str, v: &RecVal) -> R<[Chunk<'static>; 2]> {
|
||||
let [m, node] = t.record(name)?;
|
||||
@@ -681,32 +638,17 @@ pub(crate) fn build_record(m: Chunk<'static>, mut node: Chunk<'static>, layout:
|
||||
put_u8(tdb4, 79, 0)?;
|
||||
}
|
||||
}
|
||||
let mut list = key_list.clone();
|
||||
let count = u32::try_from(v.keys.len()).map_err(|_| tpl("too many keys"))?;
|
||||
{
|
||||
let lhd3 = child_data(&mut list, b"lhd3")?;
|
||||
crate::export::synth::put_key_counts(lhd3, count, u32::try_from(size).map_err(|_| tpl("record size"))?)?;
|
||||
}
|
||||
let mut ldat = Vec::with_capacity(size * v.keys.len());
|
||||
for k in &v.keys {
|
||||
ldat.extend(match layout {
|
||||
Layout::Point => spatial_key(k, n, true, size)?,
|
||||
Layout::Color => spatial_key(k, n, false, size)?,
|
||||
Layout::Point => spatial_record(k, n, true, size)?,
|
||||
Layout::Color => spatial_record(k, n, false, size)?,
|
||||
_ => crate::export::emit::plain_record(n, k, size)?,
|
||||
});
|
||||
}
|
||||
{
|
||||
let lk = kids_mut(&mut list)?;
|
||||
match lk.iter_mut().find(|c| c.is(b"ldat")) {
|
||||
Some(c) => *c = Chunk::data(*b"ldat", ldat),
|
||||
None => lk.push(Chunk::data(*b"ldat", ldat)),
|
||||
}
|
||||
}
|
||||
let kids = kids_mut(&mut node)?;
|
||||
let at = kids.iter().position(|c| c.is(b"cdat")).ok_or_else(|| tpl("shape record without cdat"))?;
|
||||
if let Some(slot) = kids.get_mut(at) {
|
||||
*slot = list;
|
||||
}
|
||||
let mut list = key_list.clone();
|
||||
fill_key_list(&mut list, ldat, v.keys.len(), size)?;
|
||||
replace_cdat(&mut node, list)?;
|
||||
}
|
||||
if let Some((text, enabled)) = &v.expr {
|
||||
// As on the Transform properties (R15; 856).
|
||||
@@ -746,7 +688,7 @@ pub(crate) fn build_items(t: &ShapeProtos, items: &[ShItem], depth: usize) -> R<
|
||||
if depth > MAX_DEPTH {
|
||||
return Ok(out);
|
||||
}
|
||||
let pos = |kids: &[Chunk<'static>], n: &str| kids.iter().position(|c| c.is(b"tdmn") && tdmn_name(c).as_deref() == Some(n));
|
||||
let pos = |kids: &[Chunk<'static>], n: &str| kids.iter().position(|c| is_tdmn(c, n));
|
||||
for item in items {
|
||||
match item {
|
||||
ShItem::Off(inner) => {
|
||||
@@ -766,7 +708,7 @@ pub(crate) fn build_items(t: &ShapeProtos, items: &[ShItem], depth: usize) -> R<
|
||||
let vg = pos(kids, "ADBE Vectors Group").ok_or_else(|| tpl("group prototype without contents"))?;
|
||||
let contents = kids.get_mut(vg + 1).ok_or_else(|| tpl("group prototype without contents"))?;
|
||||
let ck = kids_mut(contents)?;
|
||||
let end = ck.iter().position(|c| c.is(b"tdmn") && tdmn_name(c).as_deref() == Some("ADBE Group End")).unwrap_or(ck.len());
|
||||
let end = ck.iter().position(|c| is_tdmn(c, GROUP_END)).unwrap_or(ck.len());
|
||||
ck.splice(end..end, children);
|
||||
let tg = pos(kids, "ADBE Vector Transform Group").ok_or_else(|| tpl("group prototype without transform"))?;
|
||||
let tnode = kids.get_mut(tg + 1).ok_or_else(|| tpl("group prototype without transform"))?;
|
||||
@@ -815,13 +757,10 @@ pub(crate) fn put_contents(layr: &mut Chunk<'static>, t: &ShapeProtos, items: &[
|
||||
let built = build_items(t, items, 0)?;
|
||||
let root = list_mut(layr, b"tdgp")?;
|
||||
let kids = kids_mut(root)?;
|
||||
let at = kids
|
||||
.iter()
|
||||
.position(|c| c.is(b"tdmn") && tdmn_name(c).as_deref() == Some("ADBE Root Vectors Group"))
|
||||
.ok_or_else(|| tpl("shape layer without contents"))?;
|
||||
let at = kids.iter().position(|c| is_tdmn(c, "ADBE Root Vectors Group")).ok_or_else(|| tpl("shape layer without contents"))?;
|
||||
let rv = kids.get_mut(at + 1).ok_or_else(|| tpl("shape layer without contents"))?;
|
||||
let rk = kids_mut(rv)?;
|
||||
let end = rk.iter().position(|c| c.is(b"tdmn") && tdmn_name(c).as_deref() == Some("ADBE Group End")).unwrap_or(rk.len());
|
||||
let end = rk.iter().position(|c| is_tdmn(c, GROUP_END)).unwrap_or(rk.len());
|
||||
rk.splice(end..end, built);
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,14 +4,14 @@
|
||||
//! e1-sparse).
|
||||
|
||||
use crate::Chunk;
|
||||
use crate::export::synth::{KeyTemplate, NO_NAME, RecSpec, class, tdbs, tdmn, tdsn};
|
||||
use crate::export::synth::{GROUP_END, KeyTemplate, NO_NAME, RecSpec, class, tdbs, tdmn, tdmn_name, tdsn};
|
||||
|
||||
/// A group: its match name and a `tdgp` with the state `state` (`tdsb`), no user name, `kids` and
|
||||
/// the closing `ADBE Group End`.
|
||||
pub(crate) fn group(name: &str, state: u32, kids: Vec<Chunk<'static>>) -> [Chunk<'static>; 2] {
|
||||
let mut body = vec![Chunk::data(*b"tdsb", state.to_be_bytes().to_vec()), tdsn(NO_NAME)];
|
||||
body.extend(kids);
|
||||
body.push(tdmn("ADBE Group End"));
|
||||
body.push(tdmn(GROUP_END));
|
||||
[tdmn(name), Chunk::list(*b"tdgp", body)]
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ pub(crate) fn av_layer() -> Vec<Chunk<'static>> {
|
||||
let mut root = vec![Chunk::data(*b"tdsb", 1u32.to_be_bytes().to_vec()), tdsn(""), tdmn("ADBE Time Remapping"), time_remap()];
|
||||
root.extend(group("ADBE Transform Group", 1, crate::export::tree_spec::build(crate::export::tree_defs::AV_TRANSFORM)));
|
||||
root.extend(layer_styles());
|
||||
root.push(tdmn("ADBE Group End"));
|
||||
root.push(tdmn(GROUP_END));
|
||||
let layr = Chunk::list(
|
||||
*b"Layr",
|
||||
vec![Chunk::data(*b"ldta", vec![0; 164]), Chunk::data(*b"Utf8", Vec::new()), Chunk::list(*b"tdgp", root), crate::export::base::guides()],
|
||||
@@ -78,7 +78,7 @@ pub(crate) fn av_layer() -> Vec<Chunk<'static>> {
|
||||
pub(crate) fn layer_block(nodes: &[crate::export::tree_spec::Node]) -> Vec<Chunk<'static>> {
|
||||
let mut root = vec![Chunk::data(*b"tdsb", 1u32.to_be_bytes().to_vec()), tdsn("")];
|
||||
root.extend(crate::export::tree_spec::build(nodes));
|
||||
root.push(tdmn("ADBE Group End"));
|
||||
root.push(tdmn(GROUP_END));
|
||||
let layr = Chunk::list(
|
||||
*b"Layr",
|
||||
vec![Chunk::data(*b"ldta", vec![0; 164]), Chunk::data(*b"Utf8", Vec::new()), Chunk::list(*b"tdgp", root), crate::export::base::guides()],
|
||||
@@ -125,7 +125,7 @@ pub(crate) fn text_layer(doc: &[u8]) -> Vec<Chunk<'static>> {
|
||||
root.extend(group("ADBE Text Properties", 1, props));
|
||||
root.extend(group("ADBE Transform Group", 1, crate::export::tree_spec::build(crate::export::tree_defs::AV_TRANSFORM)));
|
||||
root.extend(layer_styles());
|
||||
root.push(tdmn("ADBE Group End"));
|
||||
root.push(tdmn(GROUP_END));
|
||||
let layr = Chunk::list(
|
||||
*b"Layr",
|
||||
vec![Chunk::data(*b"ldta", vec![0; 164]), Chunk::data(*b"Utf8", Vec::new()), Chunk::list(*b"tdgp", root), crate::export::base::guides()],
|
||||
@@ -141,7 +141,7 @@ pub(crate) fn pairs(nodes: &[crate::export::tree_spec::Node]) -> Vec<(String, [C
|
||||
let mut out = Vec::new();
|
||||
let mut it = crate::export::tree_spec::build(nodes).into_iter();
|
||||
while let (Some(m), Some(n)) = (it.next(), it.next()) {
|
||||
let name = String::from_utf8_lossy(m.bytes().unwrap_or_default().split(|b| *b == 0).next().unwrap_or_default()).into_owned();
|
||||
let name = tdmn_name(&m).unwrap_or_default();
|
||||
out.push((name, [m, n]));
|
||||
}
|
||||
out
|
||||
|
||||
@@ -23,8 +23,10 @@ use effectcraft_project::build::Ids;
|
||||
use effectcraft_project::styles::{self as st, STYLE_BLEND_MODES};
|
||||
use effectcraft_project::{Comp, Layer, PropGroup, Property};
|
||||
|
||||
use crate::export::keys::{fill_key_list, replace_cdat, timed_keys};
|
||||
use crate::export::plan::{CompPlan, EaseSlot, KeyPlan};
|
||||
use crate::export::report::{Collector, LossCategory};
|
||||
use crate::export::synth::{GROUP_END, is_tdmn};
|
||||
use crate::export::tree::{R, child_data, kids_mut, put_f64, put_u8, put_u32, tpl};
|
||||
|
||||
/// A record's value.
|
||||
@@ -128,20 +130,7 @@ fn plan_record(spec: &StyleParam, full: String, prop: &Property, cp: &CompPlan,
|
||||
/// The keys of a scalar in the layer's time base: linear, hold and bezier sides with the ease in
|
||||
/// stored units per second (the plain template of 976).
|
||||
fn plan_number_keys(prop: &Property, cp: &CompPlan, what: &str, col: &mut Collector) -> Vec<KeyPlan> {
|
||||
let mut timed: Vec<(i64, &effectcraft_keyframe::Keyframe)> = Vec::with_capacity(prop.keys.len());
|
||||
for k in &prop.keys {
|
||||
let (u, inexact) = cp.tb.units(k.time);
|
||||
if inexact {
|
||||
col.lose(LossCategory::RoundedTimes, format!("{what}: key at {:.4} s moved to the time base", k.time.seconds()));
|
||||
}
|
||||
timed.push((u, k));
|
||||
}
|
||||
timed.sort_by_key(|(u, _)| *u);
|
||||
let before = timed.len();
|
||||
timed.dedup_by_key(|(u, _)| *u);
|
||||
if timed.len() != before {
|
||||
col.lose(LossCategory::ApproximatedKeyframes, format!("{what}: keys at the same time were merged"));
|
||||
}
|
||||
let timed = timed_keys(&prop.keys, &cp.tb, what, "key", col);
|
||||
let mut out = Vec::with_capacity(timed.len());
|
||||
let beyond = std::cell::Cell::new(false);
|
||||
for (u, k) in timed {
|
||||
@@ -279,11 +268,6 @@ pub(crate) struct StyleProtos {
|
||||
keyed: Chunk<'static>,
|
||||
}
|
||||
|
||||
fn tdmn_name(c: &Chunk<'_>) -> Option<String> {
|
||||
let d = c.bytes()?;
|
||||
Some(d.iter().take_while(|b| **b != 0).map(|b| char::from(*b)).collect())
|
||||
}
|
||||
|
||||
impl StyleProtos {
|
||||
/// The records built in code from the style table (`style_defs`); the keyed form of a scalar
|
||||
/// takes `tdb4` bytes 5, 68 and 79 and the key list of a keyed one-component record.
|
||||
@@ -322,28 +306,13 @@ impl StyleProtos {
|
||||
}
|
||||
}
|
||||
let mut list = self.keyed.child_list(b"list").cloned().ok_or_else(|| tpl("keyed style prototype without keys"))?;
|
||||
let n = u32::try_from(keys.len()).map_err(|_| tpl("too many keys"))?;
|
||||
let size = 48usize;
|
||||
{
|
||||
let lhd3 = child_data(&mut list, b"lhd3")?;
|
||||
crate::export::synth::put_key_counts(lhd3, n, 48)?;
|
||||
}
|
||||
let mut ldat = Vec::with_capacity(size * keys.len());
|
||||
for k in keys {
|
||||
ldat.extend(crate::export::emit::plain_record(1, k, size)?);
|
||||
}
|
||||
{
|
||||
let lk = kids_mut(&mut list)?;
|
||||
match lk.iter_mut().find(|c| c.is(b"ldat")) {
|
||||
Some(c) => *c = Chunk::data(*b"ldat", ldat),
|
||||
None => lk.push(Chunk::data(*b"ldat", ldat)),
|
||||
}
|
||||
}
|
||||
let kids = kids_mut(&mut rec)?;
|
||||
let at = kids.iter().position(|c| c.is(b"cdat")).ok_or_else(|| tpl("style record without cdat"))?;
|
||||
if let Some(slot) = kids.get_mut(at) {
|
||||
*slot = list;
|
||||
}
|
||||
fill_key_list(&mut list, ldat, keys.len(), size)?;
|
||||
replace_cdat(&mut rec, list)?;
|
||||
}
|
||||
}
|
||||
if let Some((text, enabled)) = &r.expr {
|
||||
@@ -362,7 +331,7 @@ impl StyleProtos {
|
||||
|
||||
/// The node after the `tdmn` named `name` among `kids`.
|
||||
fn named_mut<'a>(kids: &'a mut [Chunk<'static>], name: &str) -> R<&'a mut Chunk<'static>> {
|
||||
let i = kids.iter().position(|c| c.is(b"tdmn") && tdmn_name(c).as_deref() == Some(name)).ok_or_else(|| tpl("layer styles group incomplete"))?;
|
||||
let i = kids.iter().position(|c| is_tdmn(c, name)).ok_or_else(|| tpl("layer styles group incomplete"))?;
|
||||
kids.get_mut(i + 1).ok_or_else(|| tpl("layer styles group incomplete"))
|
||||
}
|
||||
|
||||
@@ -377,7 +346,7 @@ fn insert_records(protos: &StyleProtos, g: &mut Chunk<'static>, recs: &[Record],
|
||||
built.extend(protos.build(r)?);
|
||||
}
|
||||
let kids = kids_mut(g)?;
|
||||
let at = kids.iter().position(|c| c.is(b"tdmn") && tdmn_name(c).as_deref() == Some(before)).ok_or_else(|| tpl("layer styles group incomplete"))?;
|
||||
let at = kids.iter().position(|c| is_tdmn(c, before)).ok_or_else(|| tpl("layer styles group incomplete"))?;
|
||||
kids.splice(at..at, built);
|
||||
Ok(())
|
||||
}
|
||||
@@ -392,12 +361,12 @@ pub(crate) fn put_styles(protos: &StyleProtos, layr: &mut Chunk<'static>, plan:
|
||||
set_flags(bo, sm::FLAGS_ON)?;
|
||||
insert_records(protos, bo, &plan.global, sm::ADV_BLEND)?;
|
||||
let adv = named_mut(kids_mut(bo)?, sm::ADV_BLEND)?;
|
||||
insert_records(protos, adv, &plan.adv, "ADBE Group End")?;
|
||||
insert_records(protos, adv, &plan.adv, GROUP_END)?;
|
||||
}
|
||||
for (prefix, recs, on) in &plan.styles {
|
||||
let g = named_mut(kids_mut(ls)?, &format!("{prefix}/enabled"))?;
|
||||
set_flags(g, if *on { sm::FLAGS_ON } else { sm::FLAGS_OFF })?;
|
||||
insert_records(protos, g, recs, "ADBE Group End")?;
|
||||
insert_records(protos, g, recs, GROUP_END)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,9 +4,10 @@
|
||||
//! evidence, or zero where After Effects was shown to accept zero (the read-backs listed in
|
||||
//! `docs/aep-export.md`, "Built in code").
|
||||
|
||||
use crate::export::tree::{Fixed, R, put_u32};
|
||||
use crate::{Body, Chunk};
|
||||
|
||||
/// `tdmn`: a match name padded to 40 bytes.
|
||||
/// `tdmn`: a match name padded to 40 bytes (at most 39 bytes of the name, so that a NUL remains).
|
||||
pub(crate) fn tdmn(name: &str) -> Chunk<'static> {
|
||||
let mut b = vec![0u8; 40];
|
||||
for (slot, c) in b.iter_mut().zip(name.bytes().take(39)) {
|
||||
@@ -15,12 +16,34 @@ pub(crate) fn tdmn(name: &str) -> Chunk<'static> {
|
||||
Chunk::data(*b"tdmn", b)
|
||||
}
|
||||
|
||||
/// The match name that closes a property group.
|
||||
pub(crate) const GROUP_END: &str = "ADBE Group End";
|
||||
|
||||
/// The match name in a `tdmn`: the bytes up to the first NUL as UTF-8, invalid sequences
|
||||
/// replaced, as the importer reads it (`container::typed`). After Effects writes ASCII here
|
||||
/// (aep-format.md 3.4; every `tdmn` of the corpus is ASCII); `None` when `c` is not a data chunk.
|
||||
pub(crate) fn tdmn_name(c: &Chunk<'_>) -> Option<String> {
|
||||
let b = c.bytes()?;
|
||||
let end = b.iter().position(|x| *x == 0).unwrap_or(b.len());
|
||||
Some(String::from_utf8_lossy(b.get(..end)?).into_owned())
|
||||
}
|
||||
|
||||
/// Whether `c` is a `tdmn` naming `name`.
|
||||
pub(crate) fn is_tdmn(c: &Chunk<'_>, name: &str) -> bool {
|
||||
c.is(b"tdmn") && tdmn_name(c).as_deref() == Some(name)
|
||||
}
|
||||
|
||||
/// The user name of a property that has none (`-_0_/-`, aep-format.md 3.4).
|
||||
pub(crate) const NO_NAME: &str = "-_0_/-";
|
||||
|
||||
/// `tdsn`: the user name, a nested chunk holding one `Utf8`.
|
||||
pub(crate) fn tdsn(name: &str) -> Chunk<'static> {
|
||||
Chunk { id: *b"tdsn", body: Body::Chunks(vec![Chunk::data(*b"Utf8", name.as_bytes().to_vec())]), pad: None }
|
||||
utf8_in(*b"tdsn", name)
|
||||
}
|
||||
|
||||
/// A nested chunk `id` holding one `Utf8` with `text` (`tdsn`, `fnam`, `pdnm`).
|
||||
pub(crate) fn utf8_in(id: [u8; 4], text: &str) -> Chunk<'static> {
|
||||
Chunk { id, body: Body::Chunks(vec![Chunk::data(*b"Utf8", text.as_bytes().to_vec())]), pad: None }
|
||||
}
|
||||
|
||||
/// How a property's keys are stored (`tdb4` byte 7, aep-format.md 3.5).
|
||||
@@ -100,13 +123,12 @@ impl RecSpec {
|
||||
/// word and the constant doubles at 16..56 are written; After Effects reads the other bytes as
|
||||
/// zero (read-back e1-ztdb4d).
|
||||
pub(crate) fn tdb4(spec: &RecSpec, keyed: bool, time_scale: u32) -> Vec<u8> {
|
||||
let mut b = vec![0u8; 124];
|
||||
let set = |b: &mut Vec<u8>, at: usize, bytes: &[u8]| {
|
||||
if let Some(dst) = b.get_mut(at..at + bytes.len()) {
|
||||
dst.copy_from_slice(bytes);
|
||||
}
|
||||
};
|
||||
set(&mut b, 0, &[0xdb, 0x99, 0x00, spec.dims]);
|
||||
tdb4_layout(spec, keyed, time_scale).finish(|| "tdb4".to_string())
|
||||
}
|
||||
|
||||
fn tdb4_layout(spec: &RecSpec, keyed: bool, time_scale: u32) -> Fixed {
|
||||
let mut b = Fixed::zeroed(124);
|
||||
b.put(0, &[0xdb, 0x99, 0x00, spec.dims]);
|
||||
let spatial = match spec.template {
|
||||
KeyTemplate::Plain => 0,
|
||||
KeyTemplate::Spatial(t) => t,
|
||||
@@ -115,19 +137,19 @@ pub(crate) fn tdb4(spec: &RecSpec, keyed: bool, time_scale: u32) -> Vec<u8> {
|
||||
Some(b) => (b & !0x01) | u8::from(!keyed),
|
||||
None => (if spatial == 0 { 0 } else { spatial_byte5(spatial) }) | u8::from(!keyed),
|
||||
};
|
||||
set(&mut b, 5, &[byte5]);
|
||||
set(&mut b, 7, &[spatial]);
|
||||
set(&mut b, 8, &spec.class.to_be_bytes());
|
||||
set(&mut b, 12, &time_scale.to_be_bytes());
|
||||
b.put(5, &[byte5]);
|
||||
b.put(7, &[spatial]);
|
||||
b.put(8, &spec.class.to_be_bytes());
|
||||
b.put(12, &time_scale.to_be_bytes());
|
||||
// Every record After Effects writes carries 0.0001 and four 1.0 here; without them a roving
|
||||
// key reads back at time zero instead of its retimed place (read-back s14-re-keyframes).
|
||||
set(&mut b, 16, &1e-4f64.to_be_bytes());
|
||||
b.put(16, &1e-4f64.to_be_bytes());
|
||||
for at in [24, 32, 40, 48] {
|
||||
set(&mut b, at, &1.0f64.to_be_bytes());
|
||||
b.put(at, &1.0f64.to_be_bytes());
|
||||
}
|
||||
set(&mut b, 68, &[u8::from(keyed)]);
|
||||
b.put(68, &[u8::from(keyed)]);
|
||||
// Set on a static spatial property, cleared once it is keyed (aep-format.md 3.4, byte 79).
|
||||
set(&mut b, 79, &[u8::from(spatial != 0 && !keyed)]);
|
||||
b.put(79, &[u8::from(spatial != 0 && !keyed)]);
|
||||
b
|
||||
}
|
||||
|
||||
@@ -135,16 +157,20 @@ pub(crate) fn tdb4(spec: &RecSpec, keyed: bool, time_scale: u32) -> Vec<u8> {
|
||||
/// 3.5): word 0 `0x00d00bee`, word 2 the count, word 3 the blocks of four (at least one), word 4
|
||||
/// the size, words 5 and 7 the capacity (the count rounded up to four, at least two), word 6 one.
|
||||
pub(crate) fn key_list(size: u32, count: u32, records: Vec<u8>) -> Chunk<'static> {
|
||||
let mut lhd3 = vec![0u8; 52];
|
||||
// Cannot fail: the header is 52 bytes.
|
||||
let _ = crate::export::tree::put_u32(&mut lhd3, 0, 0x00d0_0bee);
|
||||
let _ = crate::export::tree::put_u32(&mut lhd3, 24, 1);
|
||||
let _ = put_key_counts(&mut lhd3, count, size);
|
||||
let lhd3 = key_list_header(size, count).finish(|| "lhd3".to_string());
|
||||
Chunk::list(*b"list", vec![Chunk::data(*b"lhd3", lhd3), Chunk::data(*b"ldat", records)])
|
||||
}
|
||||
|
||||
fn key_list_header(size: u32, count: u32) -> Fixed {
|
||||
let mut lhd3 = Fixed::zeroed(52);
|
||||
lhd3.put(0, &0x00d0_0bee_u32.to_be_bytes());
|
||||
lhd3.put(24, &1u32.to_be_bytes());
|
||||
lhd3.apply(|d| put_key_counts(d, count, size));
|
||||
lhd3
|
||||
}
|
||||
|
||||
/// Sets the count words of a key list header for `count` keys of `size` bytes.
|
||||
pub(crate) fn put_key_counts(lhd3: &mut [u8], count: u32, size: u32) -> crate::export::tree::R<()> {
|
||||
pub(crate) fn put_key_counts(lhd3: &mut [u8], count: u32, size: u32) -> R<()> {
|
||||
let capacity = count.div_ceil(4).saturating_mul(4).max(2);
|
||||
put_counts(lhd3, count, count.div_ceil(4).max(1), size, capacity, capacity)
|
||||
}
|
||||
@@ -153,8 +179,7 @@ pub(crate) fn put_key_counts(lhd3: &mut [u8], count: u32, size: u32) -> crate::e
|
||||
/// record size, word 5 `w5`, word 7 the capacity. A key list has `w5` = the capacity; a path
|
||||
/// list counts vertices: `blocks` = `w5` = the vertex count, the capacity four times it (090:
|
||||
/// 12 points, 4, 4 and 16; aep-format.md 3.5).
|
||||
pub(crate) fn put_counts(lhd3: &mut [u8], count: u32, blocks: u32, size: u32, w5: u32, capacity: u32) -> crate::export::tree::R<()> {
|
||||
use crate::export::tree::put_u32;
|
||||
pub(crate) fn put_counts(lhd3: &mut [u8], count: u32, blocks: u32, size: u32, w5: u32, capacity: u32) -> R<()> {
|
||||
put_u32(lhd3, 8, count)?;
|
||||
put_u32(lhd3, 12, blocks)?;
|
||||
put_u32(lhd3, 16, size)?;
|
||||
@@ -204,6 +229,36 @@ mod tests {
|
||||
assert_eq!(rec.child(b"cdat").and_then(|c| c.bytes()).map(<[u8]>::len), Some(40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_record_header_fits() {
|
||||
for dims in 0..=u8::MAX {
|
||||
for template in [KeyTemplate::Plain, KeyTemplate::Spatial(1), KeyTemplate::Spatial(3), KeyTemplate::Spatial(u8::MAX)] {
|
||||
for byte5 in [None, Some(0xff)] {
|
||||
let spec = RecSpec { dims, template, class: u32::MAX, limits: None, byte5 };
|
||||
assert!(tdb4_layout(&spec, false, u32::MAX).error().is_none() && tdb4_layout(&spec, true, 0).error().is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (size, count) in [(0, 0), (u32::MAX, u32::MAX), (48, 5)] {
|
||||
let h = key_list_header(size, count);
|
||||
assert!(h.error().is_none());
|
||||
assert_eq!(h.finish(String::new).len(), 52);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_match_name_reads_as_the_importer_reads_it() {
|
||||
let m = tdmn("ADBE Group End");
|
||||
assert_eq!(tdmn_name(&m).as_deref(), Some(GROUP_END));
|
||||
assert!(is_tdmn(&m, GROUP_END) && !is_tdmn(&m, "ADBE Group") && !is_tdmn(&tdsn(GROUP_END), GROUP_END));
|
||||
// At most 39 bytes, so that a NUL remains.
|
||||
let long = "x".repeat(50);
|
||||
assert_eq!(tdmn_name(&tdmn(&long)).map(|n| n.len()), Some(39));
|
||||
// Not ASCII (After Effects writes ASCII): UTF-8, invalid bytes replaced, as on import.
|
||||
let odd = Chunk::data(*b"tdmn", vec![b'A', 0xc3, 0xa9, 0xff, 0, b'B']);
|
||||
assert_eq!(tdmn_name(&odd).as_deref(), Some("A\u{e9}\u{fffd}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_key_list_header_counts_and_rounds() {
|
||||
let l = key_list(48, 5, vec![0; 240]);
|
||||
|
||||
@@ -21,6 +21,7 @@ use effectcraft_project::{Layer, Node, PropGroup};
|
||||
use crate::export::plan::CompPlan;
|
||||
use crate::export::report::{Collector, LossCategory};
|
||||
use crate::export::shapes::{Conv, Layout, RecVal, ShCx, build_record, c_color, c_popup, c_scalar, rec_value};
|
||||
use crate::export::synth::{GROUP_END, is_tdmn};
|
||||
use crate::export::tree::{R, kids_mut, set_utf8, tpl};
|
||||
|
||||
/// More animators or selectors than this on one layer are not written (and reported).
|
||||
@@ -219,14 +220,8 @@ fn plan_selector(s: &PropGroup, animator: &str, cx: &mut ShCx<'_>) -> SelPlan {
|
||||
SelPlan { name: crate::export::plan::cut_name(&s.name, 255, &here, cx.col), records, advanced }
|
||||
}
|
||||
|
||||
fn tdmn_name(c: &Chunk<'_>) -> Option<String> {
|
||||
let b = c.bytes()?;
|
||||
let end = b.iter().position(|x| *x == 0).unwrap_or(b.len());
|
||||
std::str::from_utf8(b.get(..end)?).ok().map(str::to_string)
|
||||
}
|
||||
|
||||
fn pos(kids: &[Chunk<'static>], n: &str) -> Option<usize> {
|
||||
kids.iter().position(|c| c.is(b"tdmn") && tdmn_name(c).as_deref() == Some(n))
|
||||
kids.iter().position(|c| is_tdmn(c, n))
|
||||
}
|
||||
|
||||
/// The item's user name (`tdsn/Utf8`).
|
||||
@@ -296,7 +291,7 @@ pub(crate) fn put_animators(layr: &mut Chunk<'static>, t: &AnimProtos, anims: &[
|
||||
}
|
||||
{
|
||||
let gk = kids_mut(&mut group)?;
|
||||
let end = pos(gk, "ADBE Group End").unwrap_or(gk.len());
|
||||
let end = pos(gk, GROUP_END).unwrap_or(gk.len());
|
||||
gk.splice(end..end, built);
|
||||
}
|
||||
// Into the layer's `ADBE Text Properties`, before its Group End.
|
||||
@@ -305,7 +300,7 @@ pub(crate) fn put_animators(layr: &mut Chunk<'static>, t: &AnimProtos, anims: &[
|
||||
let ti = pos(rk, "ADBE Text Properties").ok_or_else(|| tpl("text layer without text properties"))?;
|
||||
let tp = rk.get_mut(ti + 1).ok_or_else(|| tpl("text layer without text properties"))?;
|
||||
let tk = kids_mut(tp)?;
|
||||
let end = pos(tk, "ADBE Group End").unwrap_or(tk.len());
|
||||
let end = pos(tk, GROUP_END).unwrap_or(tk.len());
|
||||
tk.splice(end..end, [gm, group]);
|
||||
Ok(())
|
||||
}
|
||||
@@ -40,6 +40,7 @@ pub(crate) fn child_data<'a>(c: &'a mut Chunk<'static>, id: &[u8; 4]) -> R<&'a m
|
||||
data_mut(child_mut(c, id)?)
|
||||
}
|
||||
|
||||
/// The one byte writer of the exporter: `bytes` at `at`, or an error when they do not fit.
|
||||
pub(crate) fn put(d: &mut [u8], at: usize, bytes: &[u8]) -> R<()> {
|
||||
let end = at.checked_add(bytes.len()).ok_or_else(|| tpl("offset overflow"))?;
|
||||
let dst = d.get_mut(at..end).ok_or_else(|| tpl("chunk shorter than its layout"))?;
|
||||
@@ -90,20 +91,157 @@ pub(crate) fn put_cstr(d: &mut [u8], at: usize, len: usize, text: &str) -> R<()>
|
||||
break;
|
||||
}
|
||||
let mut buf = [0u8; 4];
|
||||
let enc = ch.encode_utf8(&mut buf);
|
||||
for (i, b) in enc.as_bytes().iter().enumerate() {
|
||||
if let Some(slot) = dst.get_mut(n + i) {
|
||||
*slot = *b;
|
||||
}
|
||||
}
|
||||
put(dst, n, ch.encode_utf8(&mut buf).as_bytes())?;
|
||||
n += l;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// How many fixed layouts on this thread did not fit their buffer (see [`Fixed`]); the tests
|
||||
/// keep it at zero.
|
||||
static FIXED_LAYOUT_ERRORS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// A buffer whose layout is fixed in code (constant size and offsets, no input from a project).
|
||||
/// Every write goes through [`put`]; a write that does not fit is skipped and its error kept, the
|
||||
/// others are written. Such an error is a bug in the layout, never something a project can cause:
|
||||
/// the tests build every one of these layouts and require no error. The builders keep their
|
||||
/// infallible signatures because files outside this module call them without a `Result`; if a
|
||||
/// layout ever broke, [`Fixed::finish`] logs it once and returns what fit.
|
||||
pub(crate) struct Fixed {
|
||||
bytes: Vec<u8>,
|
||||
error: Option<AepExportError>,
|
||||
}
|
||||
|
||||
impl Fixed {
|
||||
/// `len` zero bytes.
|
||||
pub(crate) fn zeroed(len: usize) -> Fixed {
|
||||
Fixed { bytes: vec![0u8; len], error: None }
|
||||
}
|
||||
|
||||
/// Writes `bytes` at `at`, or keeps the error (the first one) when they do not fit.
|
||||
pub(crate) fn put(&mut self, at: usize, bytes: &[u8]) {
|
||||
self.apply(|d| put(d, at, bytes));
|
||||
}
|
||||
|
||||
/// Runs a writer on the buffer, keeping its error (the first one).
|
||||
pub(crate) fn apply(&mut self, write: impl FnOnce(&mut [u8]) -> R<()>) {
|
||||
if let Err(e) = write(&mut self.bytes) {
|
||||
self.error.get_or_insert(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// The error of the first write that did not fit (for the tests).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn error(&self) -> Option<&AepExportError> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
/// The bytes; a write that did not fit is logged once, with what `layout` says.
|
||||
pub(crate) fn finish(self, layout: impl FnOnce() -> String) -> Vec<u8> {
|
||||
if let Some(e) = self.error {
|
||||
FIXED_LAYOUT_ERRORS.with(|n| n.set(n.get().saturating_add(1)));
|
||||
log::error!("aep export: the fixed layout of {} does not fit: {e}", layout());
|
||||
}
|
||||
self.bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// How many fixed layouts have failed on this thread (for the tests).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn fixed_layout_errors() -> usize {
|
||||
FIXED_LAYOUT_ERRORS.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// The entries of a property group by match name (see [`split_group`]).
|
||||
pub(crate) type Entries = Vec<(String, Vec<Chunk<'static>>)>;
|
||||
|
||||
/// Splits the children of a property group into the header (up to the first `tdmn`) and its
|
||||
/// entries by match name: `[tdmn, node]`, or `[tdmn]` for the closing `ADBE Group End`. A match
|
||||
/// name without a node is an error.
|
||||
pub(crate) fn split_group(kids: &mut Vec<Chunk<'static>>) -> R<(Vec<Chunk<'static>>, Entries)> {
|
||||
let first = kids.iter().position(|c| c.is(b"tdmn")).unwrap_or(kids.len());
|
||||
let header: Vec<Chunk<'static>> = kids.drain(..first).collect();
|
||||
let mut entries = Entries::new();
|
||||
let mut it = std::mem::take(kids).into_iter();
|
||||
while let Some(m) = it.next() {
|
||||
let name = crate::export::synth::tdmn_name(&m).unwrap_or_default();
|
||||
if name == crate::export::synth::GROUP_END {
|
||||
entries.push((name, vec![m]));
|
||||
continue;
|
||||
}
|
||||
let node = it.next().ok_or_else(|| tpl("match name without a node"))?;
|
||||
entries.push((name, vec![m, node]));
|
||||
}
|
||||
Ok((header, entries))
|
||||
}
|
||||
|
||||
/// Replaces the `Utf8` child of an item or layer with `text`.
|
||||
pub(crate) fn set_utf8(c: &mut Chunk<'static>, text: &str) -> R<()> {
|
||||
let k = child_mut(c, b"Utf8")?;
|
||||
*k = Chunk::data(*b"Utf8", text.as_bytes().to_vec());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_writer_errors_instead_of_dropping() {
|
||||
let mut d = [0u8; 4];
|
||||
assert!(put(&mut d, 2, &[1, 2]).is_ok());
|
||||
assert_eq!(d, [0, 0, 1, 2]);
|
||||
// Out of range, partly or wholly, and an offset that overflows: an error, nothing written.
|
||||
assert!(put(&mut d, 3, &[9, 9]).is_err());
|
||||
assert!(put(&mut d, 8, &[9]).is_err());
|
||||
assert!(put(&mut d, usize::MAX, &[9]).is_err());
|
||||
assert_eq!(d, [0, 0, 1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cut_string_keeps_a_nul_and_whole_characters() {
|
||||
let mut d = [0xffu8; 6];
|
||||
put_cstr(&mut d, 0, 6, "ab\u{e9}\u{e9}").unwrap();
|
||||
assert_eq!(d, [b'a', b'b', 0xc3, 0xa9, 0, 0]);
|
||||
assert!(put_cstr(&mut d, 4, 6, "x").is_err());
|
||||
}
|
||||
|
||||
/// Every layout fixed in code fits its buffer: the prototypes of every export (the project
|
||||
/// skeleton, layers, Transform, masks, effects of the whole table, footage of every kind,
|
||||
/// text, cameras, lights, shapes, styles) are built and no write went out of range.
|
||||
#[test]
|
||||
fn every_fixed_layout_fits() {
|
||||
// A delta: other tests on this thread may have counted a broken layout on purpose.
|
||||
let before = fixed_layout_errors();
|
||||
assert!(crate::export::template::Template::load().is_ok());
|
||||
assert_eq!(fixed_layout_errors(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_broken_fixed_layout_writes_what_fits_and_is_counted() {
|
||||
let before = fixed_layout_errors();
|
||||
let mut b = Fixed::zeroed(4);
|
||||
b.put(0, &[1]);
|
||||
b.put(3, &[2, 2]);
|
||||
b.put(2, &[3]);
|
||||
assert!(b.error().is_some());
|
||||
assert_eq!(b.finish(|| "a test layout".to_string()), [1, 0, 3, 0]);
|
||||
assert_eq!(fixed_layout_errors(), before + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_splits_into_header_and_entries() {
|
||||
use crate::export::synth::{tdmn, tdsn};
|
||||
let rec = || Chunk::list(*b"tdbs", Vec::new());
|
||||
let mut kids = vec![tdsn("h"), tdmn("A"), rec(), tdmn("ADBE Group End")];
|
||||
let (header, entries) = split_group(&mut kids).unwrap();
|
||||
assert_eq!(header.len(), 1);
|
||||
let names: Vec<(&str, usize)> = entries.iter().map(|(n, v)| (n.as_str(), v.len())).collect();
|
||||
assert_eq!(names, [("A", 2), ("ADBE Group End", 1)]);
|
||||
// A match name without its node is an error, not a quiet stop.
|
||||
let mut broken = vec![tdmn("A")];
|
||||
assert!(split_group(&mut broken).is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user