mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 02:27:00 -06:00
M13.2: PDF / Illustrator (.ai) / EPS vector footage (effectcraft-pdf): continuous rasterisation, layered composition import, Create Shapes from Vector Layer; SVG render tree clip groups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
30a92ba82f
commit
da0ec526f3
@@ -28,12 +28,14 @@ fn create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
});
|
||||
match op {
|
||||
"shapesFromVector" => {
|
||||
let Some(f) = footage.filter(|f| f.codec == "SVG" || f.path.to_ascii_lowercase().ends_with(".svg")) else {
|
||||
skipped.push(format!("{}: not a vector (SVG) footage layer", src.name));
|
||||
let Some(f) = footage.filter(crate::vector::is_vector) else {
|
||||
skipped.push(format!("{}: not a vector (SVG, PDF, AI or EPS) footage layer", src.name));
|
||||
continue;
|
||||
};
|
||||
let bytes = s.services.read_file(&f.path).map_err(|e| EngineError::Other(format!("cannot read {}: {e}", f.path)))?;
|
||||
let doc = effectcraft_svg::parse(&bytes).map_err(|e| EngineError::Other(format!("{}: {e}", f.path)))?;
|
||||
let doc = crate::vector::vector_doc(&f.path, &bytes, f.layer.as_ref())
|
||||
.unwrap_or_else(|| Err(format!("{}: not a vector file", f.path)))
|
||||
.map_err(EngineError::Other)?;
|
||||
let id = s.edit("Create Shapes from Vector Layer", None, |proj, st| {
|
||||
let l = crate::vector::shapes_from_vector(proj, &comp, &src, &doc);
|
||||
let id = l.id;
|
||||
|
||||
@@ -101,6 +101,17 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for path in paths {
|
||||
let bytes = match s.services.read_file(&path) {
|
||||
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) {
|
||||
Ok((comp, items)) => {
|
||||
out_comps.push(comp);
|
||||
ids.extend(items);
|
||||
}
|
||||
Err(e) => errors.push(e.to_string()),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
rest.push(path);
|
||||
continue;
|
||||
@@ -153,7 +164,9 @@ fn import(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
&& f.codec == "PSD"
|
||||
{
|
||||
match s.services.read_file(path).ok().and_then(|b| effectcraft_psd::Psd::parse(b).ok()).and_then(|d| find_psd_layer(&d, sel)) {
|
||||
Some((index, name)) => f.layer = Some(effectcraft_project::SourceLayer { index: index as u32, name, layer_size: false }),
|
||||
Some((index, name)) => {
|
||||
f.layer = Some(effectcraft_project::SourceLayer { index: index as u32, name, layer_size: false, embedded: None })
|
||||
}
|
||||
None => {
|
||||
errors.push(format!("{path}: no layer {sel}"));
|
||||
continue;
|
||||
@@ -217,6 +230,22 @@ fn import_psd_comp(s: &mut Session, path: &str, bytes: Vec<u8>, retain: bool) ->
|
||||
Ok((r.comp.0, items, r.warnings))
|
||||
}
|
||||
|
||||
/// Import 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]) -> Result<(u64, Vec<u64>)> {
|
||||
let name = std::path::Path::new(path).file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "Vector".into());
|
||||
let rate = effectcraft_time::FrameRate::FPS_29_97;
|
||||
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, rate, duration).map_err(EngineError::Other)?;
|
||||
st.project_selection = vec![r.0];
|
||||
Ok(r)
|
||||
})?;
|
||||
let mut out = vec![folder.0];
|
||||
out.extend(items.iter().map(|i| i.0));
|
||||
Ok((comp.0, out))
|
||||
}
|
||||
|
||||
/// Extensions imported as data footage.
|
||||
pub const DATA_EXTENSIONS: &[&str] = &["json", "csv", "tsv"];
|
||||
|
||||
@@ -381,7 +410,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"File...",
|
||||
["File", "Import"],
|
||||
Some("Cmd+I"),
|
||||
"{paths: [string], importAs?: footage|composition|compositionLayerSizes (Photoshop files), layer?: name|index (footage of one Photoshop layer)}",
|
||||
"{paths: [string], importAs?: footage|composition|compositionLayerSizes (Photoshop, PDF, Illustrator and EPS files), layer?: name|index (footage of one Photoshop layer)}",
|
||||
always,
|
||||
import
|
||||
),
|
||||
|
||||
@@ -156,7 +156,7 @@ fn footage_item(proj: &mut Project, cx: &mut Ctx, index: usize, name: &str) -> (
|
||||
missing: false,
|
||||
sequence: vec![],
|
||||
color_profile: None,
|
||||
layer: Some(SourceLayer { index: index as u32, name: name.to_string(), layer_size: retain && !l.rect.is_empty() }),
|
||||
layer: Some(SourceLayer { index: index as u32, name: name.to_string(), layer_size: retain && !l.rect.is_empty(), embedded: None }),
|
||||
..Default::default()
|
||||
};
|
||||
let id = proj.add_item(&format!("{name}/{file}"), Label::Lavender, Some(cx.folder), ItemKind::Footage(f));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Vector conversions behind Layer ▸ Create: Create Shapes from Vector Layer (SVG footage →
|
||||
//! shape layer), Create Shapes from Text and Create Masks from Text (glyph outlines).
|
||||
//! Vector conversions behind Layer ▸ Create: Create Shapes from Vector Layer (SVG, PDF,
|
||||
//! Illustrator and EPS footage → shape layer), Create Shapes from Text and Create Masks from
|
||||
//! Text (glyph outlines), and vector files imported as compositions (one layer per file layer).
|
||||
|
||||
use effectcraft_keyframe::{Gradient, ShapePath, Value};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
@@ -149,6 +150,74 @@ pub fn svg_contents(ids: &mut Ids, doc: &Doc) -> Vec<PropGroup> {
|
||||
if (root.opacity - 1.0).abs() > 1e-9 { vec![group(ids, "svg", items, root.opacity)] } else { items }
|
||||
}
|
||||
|
||||
/// Whether footage is a vector file (SVG / PDF / AI / EPS).
|
||||
pub fn is_vector(f: &effectcraft_project::Footage) -> bool {
|
||||
effectcraft_render::is_vector_footage(f) || f.path.to_ascii_lowercase().ends_with(".svg")
|
||||
}
|
||||
|
||||
/// The vector document of an SVG / PDF / AI / EPS file (`None` for other formats), restricted
|
||||
/// to one layer when `layer` names it.
|
||||
pub fn vector_doc(path: &str, bytes: &[u8], layer: Option<&effectcraft_project::SourceLayer>) -> Option<Result<Doc, String>> {
|
||||
if path.to_ascii_lowercase().ends_with(".svg") || effectcraft_svg::looks_like_svg(bytes) {
|
||||
return Some(effectcraft_svg::parse(bytes).map_err(|e| format!("{path}: {e}")));
|
||||
}
|
||||
effectcraft_pdf::sniff(bytes)?;
|
||||
Some(
|
||||
effectcraft_pdf::parse(bytes)
|
||||
.map(|d| match layer {
|
||||
Some(l) => effectcraft_pdf::layer_doc(&d, l.index as usize),
|
||||
None => d,
|
||||
})
|
||||
.map_err(|e| format!("{path}: {e}")),
|
||||
)
|
||||
}
|
||||
|
||||
/// Import a PDF / Illustrator / EPS document as a composition the size of its page: one footage
|
||||
/// item per file layer (each showing only that layer, continuously rasterisable), top layer
|
||||
/// first. Returns (comp, folder, items).
|
||||
pub fn import_vector_comp(
|
||||
proj: &mut Project,
|
||||
path: &str,
|
||||
bytes: &[u8],
|
||||
name: &str,
|
||||
rate: effectcraft_time::FrameRate,
|
||||
duration: effectcraft_time::Tick,
|
||||
) -> Result<(ItemId, ItemId, Vec<ItemId>), String> {
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_project::{AlphaMode, Footage, FootageKind, ItemKind, SourceLayer};
|
||||
let doc = effectcraft_pdf::parse(bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
let codec = effectcraft_pdf::codec(path, bytes).unwrap_or("PDF");
|
||||
let (w, h) = doc.pixel_size();
|
||||
let file = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
||||
let folder = proj.add_item(&format!("{name} Layers"), Label::Yellow, None, ItemKind::Folder);
|
||||
let mut comp = Comp::new(w, h, rate, duration);
|
||||
let mut items = vec![];
|
||||
let names = effectcraft_pdf::layer_names(&doc);
|
||||
for (i, lname) in names.iter().enumerate() {
|
||||
let f = Footage {
|
||||
path: path.to_string(),
|
||||
kind: FootageKind::Still,
|
||||
width: w,
|
||||
height: h,
|
||||
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() }),
|
||||
..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);
|
||||
l.name = comp.unique_layer_name(lname);
|
||||
comp.layers.insert(0, l);
|
||||
}
|
||||
let cid = proj.add_item(name, Label::Sandstone, None, ItemKind::Comp(std::sync::Arc::new(comp)));
|
||||
Ok((cid, folder, items))
|
||||
}
|
||||
|
||||
/// A copy of `src`'s transform group with fresh uids.
|
||||
fn copy_transform(proj: &mut Project, src: &Layer, dst: &mut Layer) {
|
||||
let Some(tr) = src.props.sub("transform") else { return };
|
||||
|
||||
Reference in New Issue
Block a user