mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
- Footage sources: the cancellable wrapper (#784) forwards frame_with (#752 float curves) and problem (#887); problem's default lives on the trait. - Media: a called-off decode (#784) is not a footage problem (#887). - Effect Controls rename (#953): Escape cancels (egui drops focus on Escape the same frame), the Rename item is translated; regression test. - i18n (#873): drop the /out-i18n/ ignore rule and the note about an uncommitted script. - Tests: text range selector caps (#926) re-borrow the layer; the RAM preview test (#793) sets the comp's own Resolution (#462). Co-Authored-By: Claude Opus 5.5 <[email protected]>
102 lines
4.3 KiB
Rust
102 lines
4.3 KiB
Rust
//! Footage import for EffectCraft (layer **L3**: it implements `effectcraft_render::FootageSource`).
|
|
//!
|
|
//! - [`probe`] / [`probe_bytes`]: inspect a file and describe it as an [`effectcraft_project::Footage`]
|
|
//! (video, still, image sequence or audio; size, rate, duration, streams, codec, alpha).
|
|
//! - [`MediaPool`]: decodes footage frames on demand into [`effectcraft_raster::Image`]s
|
|
//! (premultiplied f32, sRGB/Rec.709-encoded values, as the compositor expects) with a
|
|
//! memory-budgeted LRU frame cache, one decoder per source, and the decoder kept positioned for
|
|
//! sequential playback. Also audio ([`MediaPool::audio_samples`]) and thumbnails.
|
|
//!
|
|
//! Video containers (MP4/MOV, Matroska/WebM, MXF OP1a / OP-Atom) and codecs (H.264, HEVC, VP9,
|
|
//! AV1, ProRes, DNxHD, MPEG-2, MJPEG; AAC, Opus, PCM, MP3/FLAC/Vorbis) come from FilmCraft's pure-Rust crates (git dependency,
|
|
//! pinned; `plan/adr/0001`). FilmCraft types never leave this crate. Stills use the `image` crate
|
|
//! (PNG, JPEG, GIF, WebP, TIFF, BMP, OpenEXR, Targa, Radiance HDR); Photoshop documents (merged image or one layer) come
|
|
//! from `effectcraft-psd` and SVG from `effectcraft-svg` (rasterised at any scale).
|
|
|
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
|
|
mod convert;
|
|
pub mod downmix;
|
|
mod dpx;
|
|
pub mod exr_channels;
|
|
mod gif_anim;
|
|
mod layered;
|
|
pub use layered::vector_doc;
|
|
mod pool;
|
|
mod probe;
|
|
mod stream;
|
|
mod webp_anim;
|
|
|
|
pub use pool::{DEFAULT_BUDGET, MediaPool, PoolStats};
|
|
pub use probe::{DEFAULT_SEQUENCE_RATE, probe, probe_bytes, probe_model};
|
|
|
|
/// Errors from probing or decoding footage.
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum MediaError {
|
|
#[error("I/O error: {0}")]
|
|
Io(String),
|
|
#[error("unsupported media: {0}")]
|
|
Unsupported(String),
|
|
#[error("decode error: {0}")]
|
|
Decode(String),
|
|
/// The read was called off before it finished (see [`effectcraft_render::FootageSource::cancellable`]).
|
|
#[error("cancelled")]
|
|
Cancelled,
|
|
}
|
|
|
|
pub type Result<T> = std::result::Result<T, MediaError>;
|
|
|
|
impl From<filmcraft_media::MediaError> for MediaError {
|
|
fn from(e: filmcraft_media::MediaError) -> Self {
|
|
use filmcraft_media::MediaError as F;
|
|
match e {
|
|
F::Unsupported(s) => MediaError::Unsupported(s),
|
|
F::Io(s) | F::Offline(s) => MediaError::Io(s),
|
|
F::Cancelled => MediaError::Cancelled,
|
|
other => MediaError::Decode(other.to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// File extensions recognised as stills.
|
|
pub const STILL_EXTENSIONS: &[&str] =
|
|
&["png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "exr", "tga", "hdr", "psd", "psb", "svg", "pdf", "ai", "eps"];
|
|
/// File extensions recognised as audio-only files.
|
|
pub const AUDIO_EXTENSIONS: &[&str] = filmcraft_media::AUDIO_EXTENSIONS;
|
|
/// File extensions recognised as movies.
|
|
pub const VIDEO_EXTENSIONS: &[&str] = &["mp4", "m4v", "mov", "mkv", "webm", "mxf"];
|
|
|
|
/// File extensions recognised as 3D models (Advanced 3D model layers).
|
|
pub const MODEL_EXTENSIONS: &[&str] = &["gltf", "glb", "obj", "stl", "fbx"];
|
|
|
|
/// Whether `path` has an extension we can import.
|
|
pub fn is_importable(path: &std::path::Path) -> bool {
|
|
let ext = ext_of(path);
|
|
MODEL_EXTENSIONS.contains(&ext.as_str())
|
|
|| STILL_EXTENSIONS.contains(&ext.as_str())
|
|
|| AUDIO_EXTENSIONS.contains(&ext.as_str())
|
|
|| VIDEO_EXTENSIONS.contains(&ext.as_str())
|
|
|| dpx::EXTENSIONS.contains(&ext.as_str())
|
|
}
|
|
|
|
pub(crate) fn ext_of(path: &std::path::Path) -> String {
|
|
path.extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase()
|
|
}
|
|
|
|
/// The `image` crate format of a still: from its signature, else from its extension (Targa has
|
|
/// no signature; a `.hdr` without the Radiance one then fails with the decoder's own error).
|
|
pub(crate) fn still_format(path: &str, bytes: &[u8]) -> Option<image::ImageFormat> {
|
|
if let Ok(fmt) = image::guess_format(bytes) {
|
|
return Some(fmt);
|
|
}
|
|
match ext_of(std::path::Path::new(path)).as_str() {
|
|
"tga" => Some(image::ImageFormat::Tga),
|
|
"hdr" => Some(image::ImageFormat::Hdr),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn rate_from_fc(r: filmcraft_time::FrameRate) -> effectcraft_time::FrameRate {
|
|
if r.num > 0 && r.den > 0 { effectcraft_time::FrameRate::new(r.num, r.den) } else { effectcraft_time::FrameRate::FPS_30 }
|
|
}
|