Merge pull request #1543 from arnav-hiwarkar/fix/1536-ai-retain-layer-sizes

Import: Composition - Retain Layer Sizes for PDF / Illustrator files (#1536)
This commit is contained in:
BFlat
2026-10-16 17:02:43 +07:00
committed by GitHub
14 changed files with 316 additions and 48 deletions
+4
View File
@@ -78,3 +78,7 @@ sync_github_name = true
[people.moecr7]
real_name = "Moein Saharkhiz"
display_name = "onlymoein"
[people.arnav-hiwarkar]
display_name = "arnav-hiwarkar"
sync_github_name = false
+3 -3
View File
@@ -391,7 +391,7 @@ fn prepare_import(s: &mut Session, p: &Value) -> Result<PendingImport> {
Ok(b) if effectcraft_psd::is_psd(&b) => b,
Ok(b) if effectcraft_pdf::sniff(&b).is_some() && !path.to_ascii_lowercase().ends_with(".svg") => {
// PDF / Illustrator / EPS: one layer per file layer.
match import_vector_comp(s, &path, &b, page) {
match import_vector_comp(s, &path, &b, page, retain) {
Ok((comp, made)) => {
comps.push(comp);
items.extend(made);
@@ -540,7 +540,7 @@ pub(crate) fn import_psd_comp(s: &mut Session, path: &str, bytes: Vec<u8>, retai
/// Import page `page` of a PDF / Illustrator / EPS file as a composition (one undo step).
/// Returns (comp, items).
fn import_vector_comp(s: &mut Session, path: &str, bytes: &[u8], page: u32) -> Result<(u64, Vec<u64>)> {
fn import_vector_comp(s: &mut Session, path: &str, bytes: &[u8], page: u32, retain: bool) -> Result<(u64, Vec<u64>)> {
let mut name = std::path::Path::new(path).file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "Vector".into());
if page > 0 {
name = format!("{name} (Page {})", page + 1);
@@ -549,7 +549,7 @@ fn import_vector_comp(s: &mut Session, path: &str, bytes: &[u8], page: u32) -> R
let secs = if s.prefs.import.still_footage == "seconds" { s.prefs.import.still_seconds } else { 10.0 };
let duration = rate.snap_nearest(effectcraft_time::Tick::from_seconds_f64(secs));
let (comp, folder, items) = s.edit("Import", None, |proj, st| {
let r = crate::vector::import_vector_comp(proj, path, bytes, &name, page, rate, duration).map_err(EngineError::Other)?;
let r = crate::vector::import_vector_comp(proj, path, bytes, &name, page, retain, (rate, duration)).map_err(EngineError::Other)?;
st.project_selection = vec![r.0];
Ok(r)
})?;
+7
View File
@@ -58,6 +58,12 @@ fn psd_layer(s: &Session, path: &str, l: &SourceLayer) -> Option<(u32, Option<(u
Some((u32::try_from(found.index).ok()?, size))
}
fn vector_size(s: &Session, f: &Footage) -> Option<(u32, u32)> {
let bytes = s.services.read_file(&f.path).ok()?;
let doc = crate::vector::vector_doc(&f.path, &bytes, f.layer.as_ref(), f.page)?.ok()?;
(!doc.root.children.is_empty()).then(|| doc.pixel_size())
}
/// Footage `f` as its file is now, given the importer's fresh look at the file (`probed`).
fn refreshed(s: &Session, f: &Footage, probed: Footage) -> Footage {
let mut nf = f.clone();
@@ -89,6 +95,7 @@ fn refreshed(s: &Session, f: &Footage, probed: Footage) -> Footage {
match &f.layer {
// A smart object's size is its embedded file's, recorded at import.
Some(l) if l.embedded.is_some() => (w, h) = (f.width, f.height),
Some(_) if crate::vector::is_vector(f) => (w, h) = vector_size(s, f).unwrap_or((f.width, f.height)),
Some(l) => match psd_layer(s, &f.path, l) {
Some((index, size)) => {
if let Some(nl) = nf.layer.as_mut() {
+18 -8
View File
@@ -271,7 +271,7 @@ pub fn vector_doc(path: &str, bytes: &[u8], layer: Option<&effectcraft_project::
Some(
effectcraft_pdf::parse_page(bytes, page as usize)
.map(|d| match layer {
Some(l) => effectcraft_pdf::layer_doc(&d, l.index as usize),
Some(l) => effectcraft_pdf::layer_doc_with_sizes(&d, l.index as usize, l.layer_size).0,
None => d,
})
.map_err(|e| format!("{path}: {e}")),
@@ -279,7 +279,7 @@ pub fn vector_doc(path: &str, bytes: &[u8], layer: Option<&effectcraft_project::
}
/// Import page `page` of a PDF / Illustrator / EPS document as a composition the size of the
/// page: one footage item per file layer (each showing only that layer, continuously
/// page: one footage item per file layer (optionally cropped to its artwork, continuously
/// rasterisable), top layer first. Returns (comp, folder, items).
pub fn import_vector_comp(
proj: &mut Project,
@@ -287,11 +287,12 @@ pub fn import_vector_comp(
bytes: &[u8],
name: &str,
page: u32,
rate: effectcraft_time::FrameRate,
duration: effectcraft_time::Tick,
retain: bool,
timing: (effectcraft_time::FrameRate, effectcraft_time::Tick),
) -> Result<(ItemId, ItemId, Vec<ItemId>), String> {
use effectcraft_color::Label;
use effectcraft_project::{AlphaMode, Footage, FootageKind, ItemKind, SourceLayer};
let (rate, duration) = timing;
let doc = effectcraft_pdf::parse_page(bytes, page as usize).map_err(|e| format!("{path}: {e}"))?;
let codec = effectcraft_pdf::codec(path, bytes).unwrap_or("PDF");
let (w, h) = doc.pixel_size();
@@ -301,24 +302,33 @@ pub fn import_vector_comp(
let mut items = vec![];
let names = effectcraft_pdf::layer_names(&doc);
for (i, lname) in names.iter().enumerate() {
let (size, offset) = if retain {
let (layer, offset) = effectcraft_pdf::layer_doc_with_sizes(&doc, i, true);
(layer.pixel_size(), offset)
} else {
((w, h), [0.0, 0.0])
};
let f = Footage {
path: path.to_string(),
kind: FootageKind::Still,
width: w,
height: h,
width: size.0,
height: size.1,
pixel_aspect: 1.0,
frame_rate: rate,
has_video: true,
alpha: AlphaMode::Straight,
loop_count: 1,
codec: codec.into(),
layer: Some(SourceLayer { index: i as u32, name: lname.clone(), layer_size: false, ..Default::default() }),
layer: Some(SourceLayer { index: i as u32, name: lname.clone(), layer_size: retain, ..Default::default() }),
page,
..Default::default()
};
let id = proj.add_item(&format!("{lname}/{file}"), Label::Lavender, Some(folder), ItemKind::Footage(f));
items.push(id);
let mut l = build::layer(proj, &comp, lname, LayerSource::Footage { item: id }, (w, h), None);
let mut l = build::layer(proj, &comp, lname, LayerSource::Footage { item: id }, size, None);
if retain && let Some(position) = l.props.prop_mut("transform/position") {
position.value = Value::Vec3([offset[0] + f64::from(size.0) / 2.0, offset[1] + f64::from(size.1) / 2.0, 0.0]);
}
l.name = comp.unique_layer_name(lname);
comp.layers.insert(0, l);
}
+91 -17
View File
@@ -260,23 +260,25 @@ fn pdf_and_ai_footage_layers_and_shapes_from_vector_layer() {
// Import As: Composition — one layer per file layer, top first, together like the page.
let path = tmp("layers.ai");
std::fs::write(&path, &bytes).unwrap();
let mut s = effectcraft_host::session();
let r = s.execute_checked("file.import", json!({"paths": [path], "importAs": "composition"})).unwrap();
let cid = ItemId(r["comps"][0].as_u64().unwrap());
let comp = s.project.comp(cid).unwrap();
let names: Vec<&str> = comp.layers.iter().map(|l| l.name.as_str()).collect();
assert_eq!(names, vec!["Art", "Sky"]);
assert_eq!((comp.width, comp.height), (120, 80));
let art = comp.layers[0].id.0;
let img = s.render(cid, effectcraft_time::Tick::ZERO, RenderOpts::default());
let d = mean_diff(&img, &direct);
assert!(d < 1.0 / 255.0, "layered comp vs page {d}");
// Each layer's footage shows only its layer.
s.execute("layer.setSwitch", json!({"layers": [art], "switch": "video", "value": false})).unwrap();
let sky = s.render(cid, effectcraft_time::Tick::ZERO, RenderOpts::default());
assert_eq!(sky.get(5, 5), img.get(5, 5));
let p = sky.get(60, 30);
assert!(p[2] > p[0], "only sky under the art {p:?}");
for import_as in ["composition", "compositionLayerSizes"] {
let mut s = effectcraft_host::session();
let r = s.execute_checked("file.import", json!({"paths": [path], "importAs": import_as})).unwrap();
let cid = ItemId(r["comps"][0].as_u64().unwrap());
let comp = s.project.comp(cid).unwrap();
let names: Vec<&str> = comp.layers.iter().map(|l| l.name.as_str()).collect();
assert_eq!(names, vec!["Art", "Sky"]);
assert_eq!((comp.width, comp.height), (120, 80));
let art = comp.layers[0].id.0;
let img = s.render(cid, effectcraft_time::Tick::ZERO, RenderOpts::default());
let d = mean_diff(&img, &direct);
assert!(d < 1.0 / 255.0, "layered comp vs page {d}");
// Each layer's footage shows only its layer.
s.execute("layer.setSwitch", json!({"layers": [art], "switch": "video", "value": false})).unwrap();
let sky = s.render(cid, effectcraft_time::Tick::ZERO, RenderOpts::default());
assert_eq!(sky.get(5, 5), img.get(5, 5));
let p = sky.get(60, 30);
assert!(p[2] > p[0], "only sky under the art {p:?}");
}
}
#[test]
@@ -522,3 +524,75 @@ fn shapes_from_vector_layer_keeps_images_as_footage_layers() {
assert!((0..3).all(|k| (a[k] - b[k]).abs() < 0.05), "({x}, {y}): {a:?} vs {b:?}");
}
}
fn retain_pdf(width: u32) -> Vec<u8> {
effectcraft_pdf::write::pdf(
&[
(1, "<< /Type /Catalog /Pages 2 0 R >>".into(), None),
(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), None),
(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 120 80] /Resources << /Properties << /L0 5 0 R /L1 6 0 R >> >> /Contents 4 0 R >>".into(), None),
(
4,
"<< >>".into(),
Some(
format!("/OC /L0 BDC q 20 15 {width} 20 re W n 1 0 0 rg 0 0 120 80 re f Q EMC /OC /L1 BDC 0 0 1 RG 4 w 70 40 m 100 40 l S EMC")
.into_bytes(),
),
),
(5, "<< /Type /OCG /Name (Box) >>".into(), None),
(6, "<< /Type /OCG /Name (Line) >>".into(), None),
],
1,
)
}
#[test]
fn ai_composition_retains_layer_sizes_and_positions() {
use effectcraft_project::{ItemKind, LayerSource, Project};
use effectcraft_time::Tick;
for name in ["retain.ai", "retain.pdf"] {
let path = tmp(name);
let bytes = retain_pdf(30);
std::fs::write(&path, &bytes).unwrap();
let mut s = effectcraft_host::session();
let r = s.execute_checked("file.import", json!({"paths": [path], "importAs": "compositionLayerSizes"})).unwrap();
assert_eq!(r["errors"], json!([]), "{r}");
let cid = ItemId(r["comps"][0].as_u64().unwrap());
let comp = s.project.comp(cid).unwrap();
assert_eq!((comp.width, comp.height), (120, 80));
assert_eq!(comp.layers.iter().map(|l| l.name.as_str()).collect::<Vec<_>>(), ["Line", "Box"]);
let mut ids = vec![];
for (name, size, anchor, position) in [("Box", (30, 20), [15.0, 10.0], [35.0, 55.0]), ("Line", (30, 4), [15.0, 2.0], [85.0, 40.0])] {
let l = comp.layer_by_name(name).unwrap();
let LayerSource::Footage { item } = l.source else { panic!("footage") };
let ItemKind::Footage(f) = &s.project.item(item).unwrap().kind else { panic!("footage") };
assert_eq!((f.width, f.height), size, "{name} uses its artwork bounds");
assert!(f.layer.as_ref().unwrap().layer_size);
assert_eq!(l.props.prop("transform/anchor").unwrap().value.as_vec2(), anchor);
assert_eq!(l.props.prop("transform/position").unwrap().value.as_vec2(), position);
let frame = s.footage.frame(item, f, Tick::ZERO).unwrap();
assert_eq!((frame.width, frame.height), size);
let sharp = s.footage.vector_frame(item, f, 2.0).unwrap();
assert_eq!((sharp.width, sharp.height), (size.0 * 2, size.1 * 2));
assert!(sharp.get(1, 1)[3] > 0.99);
ids.push(item);
}
let direct = effectcraft_svg::rasterize(&effectcraft_pdf::parse(&bytes).unwrap(), 120, 80, 1.0);
assert!(mean_diff(&s.render(cid, Tick::ZERO, RenderOpts::default()), &direct) < 0.001);
let saved = s.project.to_json();
s.project = std::sync::Arc::new(Project::from_json(&saved).unwrap());
assert!(mean_diff(&s.render(cid, Tick::ZERO, RenderOpts::default()), &direct) < 0.001, "reopened project");
let box_id = s.active_comp().unwrap().layer_by_name("Box").unwrap().id.0;
s.execute_checked("layer.create", json!({"op": "shapesFromVector", "layers": [box_id]})).unwrap();
assert!(mean_diff(&s.render(cid, Tick::ZERO, RenderOpts::default()), &direct) < 0.001, "shape conversion preserves placement");
s.execute_checked("edit.undo", json!({})).unwrap();
std::fs::write(&path, retain_pdf(40)).unwrap();
s.execute_checked("file.reloadFootage", json!({"items": ids})).unwrap();
let ItemKind::Footage(f) = &s.project.item(ids[0]).unwrap().kind else { panic!("footage") };
assert_eq!((f.width, f.height), (40, 20), "reload refreshes the layer's own dimensions");
let frame = s.footage.frame(ids[0], f, Tick::ZERO).unwrap();
assert_eq!((frame.width, frame.height), (40, 20));
assert!(frame.get(39, 10)[0] > 0.99, "reload invalidates cached pixels");
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ pub fn vector_doc(
Some(
effectcraft_pdf::parse_page(bytes, page as usize)
.map(|d| match layer {
Some(l) => effectcraft_pdf::layer_doc(&d, l.index as usize),
Some(l) => effectcraft_pdf::layer_doc_with_sizes(&d, l.index as usize, l.layer_size).0,
None => d,
})
.map_err(|e| format!("{path}: {e}")),
+3 -1
View File
@@ -4,7 +4,9 @@ Vector footage from PDF files, PDF-compatible Adobe Illustrator files (`.ai`, wh
documents) and Encapsulated PostScript (`.eps`). Files parse into the same render tree as SVG
(`effectcraft_svg::Doc`), so they rasterise at any scale (Continuously Rasterize), convert to shape
layers (Layer ▸ Create ▸ Create Shapes from Vector Layer) and import as compositions with one
layer per file layer (File ▸ Import ▸ Composition).
layer per file layer (File ▸ Import ▸ Composition). Composition - Retain Layer Sizes uses each
layer's artwork bounds and keeps its placement on the page. Images keep their rectangular bounds;
clipping paths constrain the bounds, while soft masks do not change them.
Specifications used: **ISO 32000-1:2008** (PDF 1.7; the public Adobe edition) for the file
structure, filters, content streams, graphics state, colour spaces, functions, shadings, text,
+21
View File
@@ -195,6 +195,27 @@ pub fn layer_doc(doc: &Doc, index: usize) -> Doc {
d
}
/// One layer, optionally cropped to its artwork, and its original document-space top-left.
pub fn layer_doc_with_sizes(doc: &Doc, index: usize, retain: bool) -> (Doc, [f64; 2]) {
let mut d = layer_doc(doc, index);
if !retain {
return (d, [0.0, 0.0]);
}
let page = kurbo::Rect::new(0.0, 0.0, doc.width, doc.height);
let bounds = d.artwork_bounds().map(|b| b.intersect(page)).filter(|b| b.width() > 0.0 && b.height() > 0.0);
let (x, y, w, h) = match bounds {
Some(b) => {
let (x, y) = (b.x0.floor(), b.y0.floor());
(x, y, b.x1.ceil() - x, b.y1.ceil() - y)
}
None => (0.0, 0.0, 1.0, 1.0),
};
d.width = w;
d.height = h;
d.root.transform = Affine::translate((-x, -y)) * d.root.transform;
(d, [x, y])
}
#[cfg(test)]
mod tests;
#[cfg(test)]
+45
View File
@@ -1045,3 +1045,48 @@ fn multiple_pages_and_calculator_shadings() {
assert!(img.get(5, 25)[0] < 0.1 && img.get(95, 25)[0] > 0.9, "{:?} {:?}", img.get(5, 25), img.get(95, 25));
assert_eq!(parse_page(&bytes, 2), Err(Error::PageOutOfRange(2)));
}
#[test]
fn retained_layers_keep_strokes_clips_and_empty_layers() {
let doc = parse(&sample_pdf(false)).unwrap();
for i in 0..layer_names(&doc).len() {
let full = layer_doc(&doc, i);
let (cropped, offset) = layer_doc_with_sizes(&doc, i, true);
let (w, h) = cropped.pixel_size();
let image = effectcraft_svg::rasterize(&cropped, w, h, 1.0);
let original = effectcraft_svg::rasterize(&full, 200, 100, 1.0);
for y in 0..100 {
for x in 0..200 {
let a = image.get(x - offset[0] as i64, y - offset[1] as i64);
let b = original.get(x, y);
assert!(a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-5), "layer {i} at {x},{y}: {a:?} vs {b:?}");
}
}
}
let (empty, _) = layer_doc_with_sizes(&doc, usize::MAX, true);
assert_eq!(empty.pixel_size(), (1, 1));
}
#[test]
fn retained_layers_measure_transformed_artwork_without_rasterizing() {
let doc = effectcraft_svg::parse(br#"<svg width="120" height="80"><g transform="translate(60,0)"><rect x="10" y="20" width="30" height="10" transform="rotate(90)"/></g><path d="M 10 20 L 30 20" fill="none" stroke="red" stroke-width="4" stroke-linecap="square"/></svg>"#).unwrap();
let (rotated, offset) = layer_doc_with_sizes(&doc, 0, true);
assert_eq!(offset, [30.0, 10.0]);
assert_eq!(rotated.pixel_size(), (10, 30));
let (stroked, offset) = layer_doc_with_sizes(&doc, 1, true);
assert_eq!(offset, [8.0, 18.0]);
assert_eq!(stroked.pixel_size(), (24, 4));
}
#[test]
fn retained_layers_preserve_artwork_beyond_the_bounds_depth_limit() {
let mut doc = effectcraft_svg::parse(br#"<svg width="120" height="80"><rect x="20" y="15" width="30" height="20"/></svg>"#).unwrap();
for _ in 0..300 {
let mut group = Group::new("nested");
group.children = std::mem::take(&mut doc.root.children);
doc.root.children.push(Node::Group(group));
}
let (cropped, offset) = layer_doc_with_sizes(&doc, 0, true);
assert_eq!(cropped.pixel_size(), (120, 80), "unknown bounds keep the full page");
assert_eq!(offset, [0.0, 0.0]);
}
+64
View File
@@ -0,0 +1,64 @@
use kurbo::{Affine, BezPath, Rect, Shape as _};
use crate::{Doc, Group, Node};
impl Doc {
/// Geometric artwork bounds in document pixels, including strokes and clipping.
/// Opacity and soft masks do not change the artwork's dimensions.
pub fn artwork_bounds(&self) -> Option<Rect> {
let page = Rect::new(0.0, 0.0, self.width, self.height);
group_bounds(&self.root, Affine::IDENTITY, 0, page)
}
}
fn path_bounds(path: BezPath) -> Option<Rect> {
if path.elements().is_empty() || !path.is_finite() {
return None;
}
let r = path.bounding_box();
(r.is_finite() && r.width() > 0.0 && r.height() > 0.0).then_some(r)
}
fn group_bounds(group: &Group, transform: Affine, depth: usize, page: Rect) -> Option<Rect> {
if depth > 256 {
return Some(page);
}
let transform = transform * group.transform;
if !transform.as_coeffs().iter().all(|v| v.is_finite()) {
return None;
}
let mut bounds: Option<Rect> = None;
for node in &group.children {
let rect = match node {
Node::Group(g) => group_bounds(g, transform, depth + 1, page),
Node::Image(i) => {
let rect = Rect::new(0.0, 0.0, f64::from(i.width), f64::from(i.height));
path_bounds(transform * i.transform * rect.to_path(0.01))
}
Node::Shape(s) => {
let m = transform * s.transform;
let path = s.geom.to_path();
let mut rect = s.fill.as_ref().and_then(|_| path_bounds(m * &path));
if let Some(stroke) = &s.stroke {
let scale = m.determinant().abs().sqrt();
if scale.is_finite()
&& path.is_finite()
&& let Some(outline) = effectcraft_path::stroke_outline(std::slice::from_ref(&path), &stroke.style(), scale)
&& let Some(r) = path_bounds(m * outline)
{
rect = Some(rect.map_or(r, |b| b.union(r)));
}
}
rect
}
};
if let Some(r) = rect {
bounds = Some(bounds.map_or(r, |b| b.union(r)));
}
}
let mut bounds = bounds?;
for (clip, _) in &group.clip {
bounds = bounds.intersect(path_bounds(transform * clip)?);
}
(bounds.width() > 0.0 && bounds.height() > 0.0).then_some(bounds)
}
+23
View File
@@ -6,6 +6,7 @@
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod bounds;
mod color;
mod pathdata;
mod raster;
@@ -108,6 +109,28 @@ pub struct Stroke {
pub dash: Option<(Vec<f64>, f64)>,
}
impl Stroke {
fn style(&self) -> effectcraft_path::StrokeStyle {
effectcraft_path::StrokeStyle {
width: self.width,
cap: match self.cap {
Cap::Butt => effectcraft_path::Cap::Butt,
Cap::Round => effectcraft_path::Cap::Round,
Cap::Square => effectcraft_path::Cap::Square,
},
join: match self.join {
Join::Miter => effectcraft_path::Join::Miter,
Join::Round => effectcraft_path::Join::Round,
Join::Bevel => effectcraft_path::Join::Bevel,
},
miter: self.miter,
dash: self.dash.clone(),
taper: None,
wave: None,
}
}
}
/// Shape geometry in its user space (basic shapes are kept so they can become parametric shape
/// layer items).
#[derive(Clone, Debug, PartialEq)]
+1 -17
View File
@@ -405,23 +405,7 @@ fn draw_shape(s: &Shape, m: Affine, opacity: f64, dst: &mut Image) {
paint(dst, &cov, fill, (opacity * s.fill_opacity) as f32, m, bbox);
}
if let Some(st) = &s.stroke {
let style = effectcraft_path::StrokeStyle {
width: st.width,
cap: match st.cap {
crate::Cap::Butt => effectcraft_path::Cap::Butt,
crate::Cap::Round => effectcraft_path::Cap::Round,
crate::Cap::Square => effectcraft_path::Cap::Square,
},
join: match st.join {
crate::Join::Miter => effectcraft_path::Join::Miter,
crate::Join::Round => effectcraft_path::Join::Round,
crate::Join::Bevel => effectcraft_path::Join::Bevel,
},
miter: st.miter,
dash: st.dash.clone(),
taper: None,
wave: None,
};
let style = st.style();
let cov = effectcraft_path::stroke_coverage(std::slice::from_ref(&path), &style, &mat3(m), w, h);
paint(dst, &cov, &st.paint, (opacity * st.opacity) as f32, m, bbox);
}
+10 -1
View File
@@ -228,7 +228,16 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
"file.import" if !has(p, &["importAs", "page"]) && is_pdf(p) => (
"Import PDF / Illustrator File".into(),
vec![
Field::choice("importAs", "Import Kind", &[("Footage", json!("footage")), ("Composition", json!("composition"))], 0),
Field::choice(
"importAs",
"Import Kind",
&[
("Footage", json!("footage")),
("Composition", json!("composition")),
("Composition - Retain Layer Sizes", json!("compositionLayerSizes")),
],
0,
),
Field::num("page", "Page", 1.0),
],
),
+25
View File
@@ -584,3 +584,28 @@ fn a_dropped_folder_imports_as_one_sequence_with_sequence_interpretation() {
assert!(h.state().auto.find("form.field.missingFrames").is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn illustrator_import_offers_and_applies_retain_layer_sizes() {
let path = std::env::temp_dir().join(format!("effectcraft-retain-ui-{}.ai", std::process::id()));
std::fs::write(&path, b"%PDF-1.4\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 120 80] /Contents 4 0 R >> endobj\n4 0 obj << >> stream\n1 0 0 rg 20 15 30 20 re f\nendstream endobj\n%%EOF").unwrap();
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(effectcraft_host::session()));
h.run_steps(3);
let ctx = h.ctx.clone();
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "file.import", json!({"paths": [path]})).unwrap();
h.run_steps(3);
assert!(h.query_by_label("Import PDF / Illustrator File").is_some());
click_id(&mut h, "form.field.importAs");
h.get_by_label("Composition - Retain Layer Sizes").click();
h.run_steps(2);
click_id(&mut h, "form.ok");
step_until(&mut h, |h| h.state().session.active_comp().is_some());
let s = &h.state().session;
let comp = s.active_comp().unwrap();
assert_eq!((comp.width, comp.height), (120, 80));
assert_eq!(comp.layers.len(), 1);
let effectcraft_engine::project::LayerSource::Footage { item } = comp.layers[0].source else { panic!("footage") };
let effectcraft_engine::project::ItemKind::Footage(f) = &s.project.item(item).unwrap().kind else { panic!("footage") };
assert_eq!((f.width, f.height), (30, 20));
assert!(f.layer.as_ref().unwrap().layer_size);
}