Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ At its core, Renderflow models every transformation as a **directed acyclic grap
- 🔄 **Transform pipeline** — Pluggable in-memory content transforms (built-in and custom)
- 🤖 **AI transforms** — Ollama and OpenAI-compatible LLM integration with local caching
- 🧬 **Artifact DNA** — Optional, local-first visual/layout characteristics with strict provenance, hygiene, and similarity guidance
- 🔤 **Local font registries** — Pinned font bytes, semantic typography roles, licensing metadata, and observable deterministic fallbacks
- 🖼️ **Image conversion** — FFmpeg-backed format conversion across 80+ image formats (JPEG, PNG, WebP, AVIF, HEIC, EXR, and more)
- 🎵 **Audio conversion** — FFmpeg-backed format conversion across 40+ audio formats (WAV, FLAC, MP3, AAC, Opus, and more)
- 🎬 **Whole-file video delivery** — Typed HandBrake presets with bounded execution, provenance, and explicit Aniflow temporal boundaries
Expand Down
43 changes: 36 additions & 7 deletions crates/renderflow-core/src/adapters/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use crate::artifact::{
};
use crate::assets::normalize_asset_paths;
use crate::config::OutputType;
use crate::font::{
resolve_from_variables, FontResolutionReport, FontTarget, FONT_REGISTRY_VARIABLE,
};
use crate::graph::Format;
use crate::input_format::InputFormat;
use crate::pipeline::Pipeline;
Expand All @@ -25,6 +28,7 @@ pub struct StrategyArtifactTransform {
profile: Option<String>,
variables: HashMap<String, String>,
source_asset_root: Option<PathBuf>,
font_resolution: Option<FontResolutionReport>,
cache_identity: String,
}

Expand All @@ -44,11 +48,29 @@ impl StrategyArtifactTransform {
)
})?;
let variables: HashMap<String, String> = variables.into_iter().collect();
let mut identity_variables: Vec<_> = variables.iter().collect();
let font_target = match to {
Format::Html => Some(FontTarget::Html),
Format::Pdf => Some(FontTarget::Pdf),
Format::Epub => Some(FontTarget::Epub),
Format::Docx => Some(FontTarget::Docx),
_ => None,
};
let font_resolution = font_target
.map(|target| resolve_from_variables(&variables, target))
.transpose()?
.flatten();
let font_identity = font_resolution
.as_ref()
.map(FontResolutionReport::fingerprint)
.transpose()?;
let mut identity_variables: Vec<_> = variables
.iter()
.filter(|(key, _)| key.as_str() != FONT_REGISTRY_VARIABLE)
.collect();
identity_variables.sort_by(|left, right| left.0.cmp(right.0));
let cache_identity = format!(
"renderflow.strategy-adapter/v1;from={from};to={to};template={template:?};profile={profile:?};source_root={:?};variables={identity_variables:?}",
source_asset_root
"renderflow.strategy-adapter/v1;from={from};to={to};template={template:?};profile={profile:?};source_root={:?};font_resolution={font_identity:?};variables={identity_variables:?}",
source_asset_root,
);
Ok(Self {
from,
Expand All @@ -58,6 +80,7 @@ impl StrategyArtifactTransform {
profile,
variables,
source_asset_root,
font_resolution,
cache_identity,
})
}
Expand Down Expand Up @@ -171,14 +194,20 @@ impl ArtifactTransform for StrategyArtifactTransform {
output_path.display()
);
}
store.import_path(
&output_path,
let mut descriptor =
ArtifactDescriptor::for_format(output_format, ArtifactStorageClass::Intermediate)
.with_source(input.id().clone())
.with_metadata("renderflow.adapter", "builtin.strategy")
.with_metadata("renderflow.from", self.from.to_string())
.with_metadata("renderflow.to", self.to.to_string()),
)
.with_metadata("renderflow.to", self.to.to_string());
if let Some(fonts) = &self.font_resolution {
descriptor = descriptor
.with_metadata("renderflow.font.registry", fonts.registry_id.clone())
.with_metadata("renderflow.font.target", fonts.target.to_string())
.with_metadata("renderflow.font.resolution", fonts.fingerprint()?)
.with_metadata("renderflow.font.evidence", fonts.evidence());
}
store.import_path(&output_path, descriptor)
}
}

Expand Down
19 changes: 17 additions & 2 deletions crates/renderflow-core/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use clap::Parser;
use tracing::info;

use crate::cli::{
AiCommands, AiSkillCommands, Cli, Commands, DnaCommands, EbookCommands, GraphCommands,
LuluCommands, PluginCommands, PublicationCommands, SpecCommands, ToolCommands, VideoCommands,
AiCommands, AiSkillCommands, Cli, Commands, DnaCommands, EbookCommands, FontCommands,
GraphCommands, LuluCommands, PluginCommands, PublicationCommands, SpecCommands, ToolCommands,
VideoCommands,
};
use crate::video::HandBrakeLimits;
use crate::{commands, transforms};
Expand Down Expand Up @@ -173,6 +174,20 @@ pub fn run_cli(cli: Cli) -> Result<()> {
format,
} => commands::dna::run_compare(&left, &right, output.as_deref(), &format)?,
},
Some(Commands::Font { subcommand }) => match subcommand {
FontCommands::Validate { registry, format } => {
commands::font::run_validate(&registry, &format)?
}
FontCommands::Resolve {
registry,
target,
output,
format,
} => commands::font::run_resolve(&registry, &target, output.as_deref(), &format)?,
FontCommands::Css { registry, output } => {
commands::font::run_css(&registry, output.as_deref())?
}
},
Some(Commands::Graph { subcommand }) => match subcommand {
GraphCommands::Plan {
config,
Expand Down
52 changes: 52 additions & 0 deletions crates/renderflow-core/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,20 @@ pub enum Commands {
subcommand: DnaCommands,
},

/// Validate and resolve pinned local font assets by semantic role
#[command(
subcommand_required = true,
arg_required_else_help = true,
after_help = "Examples:\n \
renderflow font validate --registry fonts.yaml\n \
renderflow font resolve --registry fonts.yaml --target pdf\n \
renderflow font css --registry fonts.yaml --output fonts.css"
)]
Font {
#[command(subcommand)]
subcommand: FontCommands,
},

/// Inspect, visualize, and export the transformation execution plan
///
/// These commands expose the canonical execution plan that the planner
Expand Down Expand Up @@ -755,6 +769,44 @@ pub enum DnaCommands {
},
}

/// Subcommands for versioned local font registries.
#[derive(Subcommand)]
pub enum FontCommands {
/// Validate registry structure, local bytes, digests, and license artifacts
Validate {
/// Font registry YAML or JSON file
#[arg(long, value_name = "FILE")]
registry: String,
/// Output format: text (default), json, or yaml
#[arg(long, default_value = "text", value_name = "FORMAT")]
format: String,
},
/// Resolve every configured semantic role for one renderer target
Resolve {
/// Font registry YAML or JSON file
#[arg(long, value_name = "FILE")]
registry: String,
/// Renderer target: html, latex, pdf, epub, or docx
#[arg(long, value_name = "TARGET")]
target: String,
/// Optional report output file
#[arg(long, value_name = "FILE")]
output: Option<String>,
/// Output format: json (default) or yaml
#[arg(long, default_value = "json", value_name = "FORMAT")]
format: String,
},
/// Emit deterministic HTML/EPUB @font-face CSS for resolved local assets
Css {
/// Font registry YAML or JSON file
#[arg(long, value_name = "FILE")]
registry: String,
/// Optional CSS output file
#[arg(long, value_name = "FILE")]
output: Option<String>,
},
}

/// Subcommands for `renderflow graph`.
#[derive(Subcommand)]
pub enum GraphCommands {
Expand Down
93 changes: 93 additions & 0 deletions crates/renderflow-core/src/commands/font.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Handlers for local font registry validation and semantic role resolution.

use std::path::Path;

use anyhow::Result;
use serde::Serialize;

use crate::font::{FontTarget, LoadedFontRegistry};

pub fn run_validate(registry: &str, format: &str) -> Result<()> {
let loaded = LoadedFontRegistry::load(registry)?;
let report = loaded.validate();
emit(&report, format)?;
if !report.valid {
anyhow::bail!("font registry validation failed");
}
Ok(())
}

pub fn run_resolve(registry: &str, target: &str, output: Option<&str>, format: &str) -> Result<()> {
let target: FontTarget = target.parse()?;
let loaded = LoadedFontRegistry::load(registry)?;
let report = loaded.resolve(target)?;
let serialized = serialize(&report, format)?;
if let Some(output) = output {
std::fs::write(output, serialized)?;
println!("Font resolution written to {}", Path::new(output).display());
} else {
print!("{serialized}");
}
Ok(())
}

pub fn run_css(registry: &str, output: Option<&str>) -> Result<()> {
let loaded = LoadedFontRegistry::load(registry)?;
let report = loaded.resolve(FontTarget::Html)?;
let css = report.css();
if let Some(output) = output {
std::fs::write(output, css)?;
println!("Local font CSS written to {}", Path::new(output).display());
} else {
print!("{css}");
}
Ok(())
}

fn emit(report: &crate::font::FontValidationReport, format: &str) -> Result<()> {
if format == "text" {
println!(
"{} {} ({})",
if report.valid { "valid" } else { "invalid" },
report.registry_id,
report.registry_digest.value
);
for diagnostic in &report.diagnostics {
println!(
"{:?} [{}]: {}",
diagnostic.severity, diagnostic.code, diagnostic.message
);
}
return Ok(());
}
print!("{}", serialize(report, format)?);
Ok(())
}

fn serialize(value: &impl Serialize, format: &str) -> Result<String> {
match format {
"json" => Ok(format!("{}\n", serde_json::to_string_pretty(value)?)),
"yaml" => Ok(serde_yaml_ng::to_string(value)?),
"text" => anyhow::bail!("text output is only available for validation"),
_ => anyhow::bail!("unknown output format '{format}'; expected text, json, or yaml"),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn serialization_rejects_unknown_format() {
let value = serde_json::json!({"valid": true});
assert!(serialize(&value, "toml").is_err());
}

#[test]
fn severity_is_available_for_text_diagnostics() {
assert_eq!(
format!("{:?}", crate::font::FontDiagnosticSeverity::Warning),
"Warning"
);
}
}
1 change: 1 addition & 0 deletions crates/renderflow-core/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod audit;
pub mod build;
pub mod dna;
pub mod ebook;
pub mod font;
pub mod graph;
pub mod inspect;
pub mod plugin;
Expand Down
Loading
Loading