mod compile;
mod de;
mod facts;
mod lower;
#[cfg(feature = "schema")]
mod schema;
pub mod spec;
#[cfg(test)]
mod tests;
mod theme;
use std::collections::BTreeMap;
use std::fmt;
use std::sync::Arc;
use alpha2_model::Project;
use serde::{Deserialize, Serialize};
pub use facts::{Facts, MediaFacts, RequiredMedia, Requirements, kind_of_path, requirements};
#[cfg(feature = "schema")]
pub use schema::compose_json_schema;
pub use spec::{COMPOSE_VERSION, VideoSpec};
pub use theme::THEMES;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ComposeError {
pub path: String,
pub message: String,
}
impl ComposeError {
pub(crate) fn new(path: &str, message: impl Into<String>) -> Self {
Self {
path: path.to_owned(),
message: message.into(),
}
}
}
impl fmt::Display for ComposeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.path, self.message)
}
}
impl std::error::Error for ComposeError {}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct LoudnessTarget {
pub target_lufs: f64,
#[serde(default = "default_ceiling")]
pub true_peak_ceiling_dbtp: f64,
}
const fn default_ceiling() -> f64 {
-1.0
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Compiled {
pub project: Project,
pub assets: BTreeMap<String, String>,
pub loudness: Option<LoudnessTarget>,
pub warnings: Vec<String>,
}
pub fn compile(
spec: &VideoSpec,
facts: &Facts,
fonts: &[Arc<[u8]>],
) -> Result<Compiled, ComposeError> {
if let Some(loudness) = spec.loudness {
let (target, ceiling) = match loudness {
spec::Loudness::Lufs(target) => (target, default_ceiling()),
spec::Loudness::Target(target) => (target.target_lufs, target.true_peak_ceiling_dbtp),
};
if !(-60.0..=0.0).contains(&target) || !(-20.0..=0.0).contains(&ceiling) {
return Err(ComposeError::new(
"spec.loudness",
format!(
"{target} LUFS under {ceiling} dBTP: the target is -60..=0 LUFS and the ceiling -20..=0 dBTP"
),
));
}
}
compile::compile(spec, facts, fonts)
}
pub fn parse_spec(json: &str) -> Result<VideoSpec, ComposeError> {
parse(json, "spec")
}
pub fn parse_facts(json: &str) -> Result<Facts, ComposeError> {
parse(json, "facts")
}
fn parse<T: serde::de::DeserializeOwned>(json: &str, root: &str) -> Result<T, ComposeError> {
let value =
serde_json::from_str(json).map_err(|error| ComposeError::new(root, error.to_string()))?;
de::from_value(value)
.map_err(|located| ComposeError::new(&de::join(root, &located.path), located.message))
}
pub fn compile_json(spec: &str, facts: &str, fonts: &[Arc<[u8]>]) -> Result<String, ComposeError> {
let spec = parse_spec(spec)?;
let facts = parse_facts(facts)?;
let compiled = compile(&spec, &facts, fonts)?;
Ok(serde_json::to_string(&compiled).expect("a compiled project serializes"))
}
pub fn requirements_json(spec: &str) -> Result<String, ComposeError> {
let spec = parse_spec(spec)?;
Ok(serde_json::to_string(&requirements(&spec)).expect("requirements serialize"))
}