Files
craft-libs/crates/aep/tests/it/container/text_style_slots.rs
Dylan M. Taylorandarnav-hiwarkar 119ac0771f Add craft-aep: After Effects project container and typed view from EffectCraft (#888)
* Add craft-aep: After Effects project container and typed view from EffectCraft

The .aep (RIFX 'Egg!') and .aepx container as a lossless chunk tree that
writes back byte for byte, and the typed, read-only view over it:
compositions, layers, properties, keyframes, effects and their settings
data, masks, shapes, text documents, markers, cameras, track mattes,
footage, project settings and the saving version.

Moved from effectcraft crates/aep/src/container at f6b4da0a with its unit
tests, the container integration tests, the aepdump and stubicc tools and
docs/aep-format.md. The match-name tables (effect_map, style_map), the
importer and the exporter are written against EffectCraft's model and
stay there. ExtractorChannels::sources takes the caller's name for a
copied channel instead of an EffectCraft constant.

* craft-aep: read the Photoshop layer a footage item shows

A footage item made from one layer of a .psd stores the layer in its
`opti` (tag `8BPS`): the layer record's index (big-endian i32 at 14, -1
for the merged image), the document's two sides (little-endian u32 at 32
and 36), the layer's bounds (little-endian i32 at 78, 82, 86, 90) and its
name (UTF-8 from 344). `Footage::psd_layer` reads them as `PsdLayer`.

The format part of storytold/effectcraft#3449; the importer's use of it
stays in EffectCraft.

---------

Co-authored-by: arnav-hiwarkar <asharnav2008@gmail.com>
2026-11-11 16:42:18 -05:00

154 lines
7.8 KiB
Rust

//! Character and paragraph slots of the text document, each found with one scripted change
//! against the reference point text 538 (`HHHH`, Times New Roman 100 px, normal baseline).
use crate::container::helpers::{first_comp, layer_named, open};
use craft_aep::{AutoKern, Justification, KernRun, Project, TextDocument};
fn text(name: &str) -> Option<TextDocument> {
Some({
let bytes = crate::common::try_fixture(name)?;
let file = open(&bytes);
let project = Project::new(&file).unwrap();
layer_named(&first_comp(&project), "T").text_document().unwrap_or_else(|| panic!("{name}: no text document"))
})
}
#[test]
fn the_reference_text_has_the_defaults_and_nothing_undecoded() {
let t = skip!(text("538-text-point-HHHH-normal-ref.aep"));
let r = t.style_runs[0];
assert_eq!((r.horizontal_scale, r.vertical_scale, r.baseline_shift, r.tsume, r.stroke_width), (1.0, 1.0, 0.0, 0.0, 1.0));
assert_eq!(r.auto_kern, AutoKern::Metrics);
assert_eq!((r.ligatures, r.no_break, r.apply_fill, r.apply_stroke, r.stroke_over_fill), (false, false, true, false, true));
assert_eq!((r.line_join, r.baseline_direction, r.digit_set), (0, 1, 0));
let p = t.paragraphs[0];
assert_eq!(p.justification, Justification::Left);
assert_eq!((p.first_line_indent, p.start_indent, p.end_indent, p.space_before, p.space_after), (0.0, 0.0, 0.0, 0.0, 0.0));
assert_eq!((p.leading_type, p.auto_hyphenate, p.hanging_roman, p.every_line_composer, p.direction), (0, true, false, false, 0));
assert!(t.kerning.is_empty());
assert_eq!(t.line_orientation, None);
assert!(t.undecoded_style_fields.is_empty() && t.undecoded_paragraph_fields.is_empty(), "{t:?}");
}
#[test]
fn character_slots_each_hold_one_setting() {
let run = |name: &str| {
Some({
let t = text(name)?;
assert!(t.undecoded_style_fields.is_empty() && t.undecoded_paragraph_fields.is_empty(), "{name}: {:?}", t.undecoded_style_fields);
t.style_runs[0]
})
};
assert_eq!(skip!(run("539-text-hscale-0.9.aep")).horizontal_scale, 0.9);
assert_eq!(skip!(run("540-text-vscale-1.5.aep")).vertical_scale, 1.5);
assert_eq!(skip!(run("541-text-baselineshift-41.aep")).baseline_shift, 41.0);
assert_eq!(skip!(run("542-text-autokern-none.aep")).auto_kern, AutoKern::None);
assert_eq!(skip!(run("543-text-autokern-optical.aep")).auto_kern, AutoKern::Optical);
assert!(skip!(run("544-text-applystroke-on.aep")).apply_stroke);
assert_eq!(skip!(run("545-text-strokewidth-5.aep")).stroke_width, 5.0);
assert!(!skip!(run("546-text-applyfill-off.aep")).apply_fill);
assert_eq!(skip!(run("548-text-linejoin-round.aep")).line_join, 1);
assert!(!skip!(run("550-text-strokeoverfill-off.aep")).stroke_over_fill);
assert!(skip!(run("552-text-nobreak-on.aep")).no_break);
assert_eq!(skip!(run("553-text-tsume-0.5.aep")).tsume, 0.5);
assert!(skip!(run("555-text-ligature-on.aep")).ligatures);
assert_eq!(skip!(run("571-text-digitset-arabic.aep")).digit_set, 1);
assert_eq!(skip!(run("578-text-digitset-hindi.aep")).digit_set, 2);
assert_eq!(skip!(run("573-text-baselinedir-rotated.aep")).baseline_direction, 2);
assert_eq!(skip!(run("576-text-baselinedir-crossstream.aep")).baseline_direction, 3);
}
#[test]
fn paragraph_slots_each_hold_one_setting() {
let para = |name: &str| {
Some({
let t = text(name)?;
assert!(t.undecoded_style_fields.is_empty() && t.undecoded_paragraph_fields.is_empty(), "{name}: {:?}", t.undecoded_paragraph_fields);
t.paragraphs[0]
})
};
assert_eq!(skip!(para("560-text-firstlineindent-20.aep")).first_line_indent, 20.0);
assert_eq!(skip!(para("561-text-startindent-30.aep")).start_indent, 30.0);
assert_eq!(skip!(para("562-text-endindent-40.aep")).end_indent, 40.0);
assert_eq!(skip!(para("563-text-spacebefore-10.aep")).space_before, 10.0);
assert_eq!(skip!(para("564-text-spaceafter-12.aep")).space_after, 12.0);
assert_eq!(skip!(para("572-text-leadingtype-japanese.aep")).leading_type, 1);
assert!(!skip!(para("556-text-autohyphenate-off.aep")).auto_hyphenate);
assert!(skip!(para("558-text-hangingroman-on.aep")).hanging_roman);
assert!(skip!(para("559-text-everyline-on.aep")).every_line_composer);
assert_eq!(skip!(para("569-text-direction-rtl.aep")).direction, 1);
assert_eq!(skip!(para("565-text-justify-fullleft.aep")).justification, Justification::FullLeft);
assert_eq!(skip!(para("566-text-justify-fullcenter.aep")).justification, Justification::FullCenter);
assert_eq!(skip!(para("567-text-justify-fullright.aep")).justification, Justification::FullRight);
assert_eq!(skip!(para("568-text-justify-fullall.aep")).justification, Justification::FullAll);
}
#[test]
fn manual_kerning_is_a_list_of_character_runs() {
// 554: kerning 50 on the whole text: automatic kerning is switched off (0) and each of the four
// characters carries 50; the closing `\r` has no amount.
let t = skip!(text("554-text-kerning-50.aep"));
assert_eq!(t.style_runs[0].auto_kern, AutoKern::None);
assert_eq!(t.kerning.iter().map(|k| k.length).sum::<usize>(), 5);
assert!(t.kerning.iter().take(4).all(|k| *k == KernRun { length: 1, amount: Some(50.0) }), "{:?}", t.kerning);
assert_eq!(t.kerning.last().and_then(|k| k.amount), None);
}
#[test]
fn vertical_text_has_its_line_orientation() {
assert_eq!(skip!(text("575-text-vertical-rtl.aep")).line_orientation, Some(2));
}
#[test]
fn several_settings_in_one_document() {
// 595: stroke on, width 5, blue, horizontal 0.9, vertical 1.5, baseline shift 41; 596 the same
// with the fill over the stroke.
let a = skip!(text("595-text-stroke-scale-shift-combined.aep")).style_runs[0];
assert_eq!((a.apply_stroke, a.stroke_width, a.horizontal_scale, a.vertical_scale, a.baseline_shift), (true, 5.0, 0.9, 1.5, 41.0));
assert_eq!(a.stroke_argb, Some([1.0, 0.0, 0.0, 1.0]));
assert!(a.stroke_over_fill);
assert!(!skip!(text("596-text-combined-fill-over-stroke.aep")).style_runs[0].stroke_over_fill);
}
#[test]
fn paragraph_slot_34_is_neutral_for_its_two_sampled_values() {
// 727: Reset Paragraph changes only /34 (0 -> 7); both values lay text out alike (S18).
assert_eq!(skip!(text("725-text-point-HHHH-normal-ref-S18.aep")).undecoded_paragraph_fields, Vec::<String>::new());
let t = skip!(text("727-text-reset-paragraph.aep"));
assert!(t.undecoded_paragraph_fields.is_empty(), "{:?}", t.undecoded_paragraph_fields);
// Another value of /34 is still listed.
let bytes = fixture!("727-text-reset-paragraph.aep");
let file = open(&bytes);
let project = Project::new(&file).unwrap();
let layer = layer_named(&first_comp(&project), "T");
let body = layer.chunk().find_list(b"btdk").and_then(|c| c.bytes()).unwrap().to_vec();
// Every `/34 7` (the run's paragraph and the document's paragraph sheet) becomes `/34 5`.
let mut changed = Vec::with_capacity(body.len());
let mut i = 0;
while i < body.len() {
if body[i..].starts_with(b"/34 7 ") {
changed.extend_from_slice(b"/34 5 ");
i += 6;
} else {
changed.push(body[i]);
i += 1;
}
}
assert_eq!(TextDocument::parse(&changed).unwrap().undecoded_paragraph_fields, ["34"]);
}
#[test]
fn slots_24_35_and_68_are_neutral_for_0_and_1() {
// 732-734: 517 with paragraph /24, paragraph /35 or style /68 edited; After Effects keeps the
// value and lays the text out the same (S21).
for name in ["732-text-box-byteedit-para24-0.aep", "733-text-box-byteedit-para35-0.aep", "734-text-box-byteedit-style68-1.aep"] {
let t = skip!(text(name));
assert!(
t.undecoded_style_fields.is_empty() && t.undecoded_paragraph_fields.is_empty(),
"{name}: {:?} {:?}",
t.undecoded_style_fields,
t.undecoded_paragraph_fields
);
}
}