Files
effectcraft/crates/media/src/layered.rs
T
Brandon ThomasandClaude Opus 5.5 94e1c86d89 IO-2/SHP-5/PRV-2: PSD import, SVG import + vector/text conversions, persistent disk cache
- effectcraft-psd (L1): PSD/PSB reader from Adobe's published file format spec (layers, groups,
  masks, blend modes, text engine data, lfx2 effects, fill/adjustment layers, vector masks;
  1/8/16/32-bit, RGB/CMYK/Gray/Lab/Indexed, raw/RLE/ZIP) and a minimal writer for fixtures.
- File > Import: importAs footage|composition|compositionLayerSizes, layer (Choose Layer);
  groups -> precomps (pass-through collapses), text -> editable text, styles -> Layer Styles,
  adjustments -> adjustment layers with effects; UI asks the Import Kind for Photoshop files.
- effectcraft-svg (L2): SVG parser (shapes, paths, transforms, use, CSS, gradients) and
  rasteriser; .svg imports as footage, continuously rasterised when the switch is on.
- layer.create: shapesFromVector, shapesFromText, masksFromText, editableText (stub removed).
- Disk cache (render::disk_cache): LZ4 content-keyed frames and layer buffers, LRU size limit
  from prefs, atomic writes, restart persistence; edit.purge clears it, cache.diskStats query;
  viewer frames read/write it and the timeline shows disk-only frames in blue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:10:48 -04:00

63 lines
2.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Layered and vector stills: Photoshop documents (`effectcraft-psd`: the merged image, or one
//! layer for footage imported as a composition) and SVG (`effectcraft-svg`, rasterised).
use effectcraft_project::{AlphaMode, Footage};
use effectcraft_raster::Image;
use crate::convert::{AlphaOp, straight_to_image};
use crate::{MediaError, Result};
fn is_svg(path: &str, bytes: &[u8]) -> bool {
path.to_ascii_lowercase().ends_with(".svg") || effectcraft_svg::looks_like_svg(bytes)
}
/// Footage for a Photoshop or SVG file (`None` for other formats).
pub(crate) fn probe(path: &str, bytes: &[u8]) -> Result<Option<Footage>> {
if effectcraft_psd::is_psd(bytes) {
let psd = effectcraft_psd::Psd::parse(bytes.to_vec()).map_err(|e| MediaError::Decode(format!("{path}: {e}")))?;
let mut f = crate::probe::still_footage(path, psd.width, psd.height, image::ImageFormat::Png, true);
f.codec = if psd.psb { "PSB".into() } else { "PSD".into() };
f.alpha = if psd.merged_alpha || !psd.layers.is_empty() { AlphaMode::Straight } else { AlphaMode::Ignore };
return Ok(Some(f));
}
if is_svg(path, bytes) {
let doc = effectcraft_svg::parse(bytes).map_err(|e| MediaError::Decode(format!("{path}: {e}")))?;
let (w, h) = doc.pixel_size();
let mut f = crate::probe::still_footage(path, w, h, image::ImageFormat::Png, true);
f.codec = "SVG".into();
f.alpha = AlphaMode::Straight;
return Ok(Some(f));
}
Ok(None)
}
/// Decode a Photoshop or SVG still (`None` for other formats).
pub(crate) fn decode(path: &str, bytes: &[u8], footage: &Footage, op: AlphaOp) -> Result<Option<Image>> {
if effectcraft_psd::is_psd(bytes) {
let psd = effectcraft_psd::Psd::parse(bytes.to_vec()).map_err(|e| MediaError::Decode(format!("{path}: {e}")))?;
let px = match &footage.layer {
Some(l) => psd.layer_pixels(l.index as usize, !l.layer_size),
None => psd.composite(),
}
.map_err(|e| MediaError::Decode(format!("{path}: {e}")))?;
return Ok(Some(straight_to_image(px.width, px.height, &px.data, op)));
}
if is_svg(path, bytes) {
let doc = effectcraft_svg::parse(bytes).map_err(|e| MediaError::Decode(format!("{path}: {e}")))?;
let (w, h) = doc.pixel_size();
let img = effectcraft_svg::rasterize(&doc, w, h, 1.0);
// The rasteriser produces premultiplied pixels already.
let _ = op;
return Ok(Some(img));
}
Ok(None)
}
/// An SVG rasterised at `scale` × its pixel size (Continuously Rasterize).
pub(crate) fn rasterize_svg(bytes: &[u8], scale: f64) -> Option<Image> {
let doc = effectcraft_svg::parse(bytes).ok()?;
let (w, h) = doc.pixel_size();
let (sw, sh) = (((w as f64 * scale).ceil() as u32).clamp(1, 16384), ((h as f64 * scale).ceil() as u32).clamp(1, 16384));
Some(effectcraft_svg::rasterize(&doc, sw, sh, scale))
}