use anyhow::{Result, bail};
use std::path::PathBuf;
use typub_ir::Document;
use crate::shared::{
DEFAULT_ASSET_ID_PREFIX, DEFAULT_ASSET_SUBDIR, DEFAULT_PNG_SCALE, RasterizeConfig,
RasterizeTarget, rasterize_document, sanitize_slug,
};
use crate::{Pass, PassCtx};
pub const SIDECAR_GENERATED_RENDER_ASSETS: &str = "render.generated_assets";
#[derive(Debug, Clone)]
pub struct RasterizeSvgToLocalAssetPass {
output_dir: PathBuf,
slug: String,
assets_subdir: String,
scale: f32,
asset_id_prefix: String,
}
impl RasterizeSvgToLocalAssetPass {
pub fn new(output_dir: PathBuf, slug: impl Into<String>) -> Self {
Self {
output_dir,
slug: sanitize_slug(&slug.into()),
assets_subdir: DEFAULT_ASSET_SUBDIR.to_string(),
scale: DEFAULT_PNG_SCALE,
asset_id_prefix: DEFAULT_ASSET_ID_PREFIX.to_string(),
}
}
pub fn with_assets_subdir(mut self, assets_subdir: impl Into<String>) -> Self {
self.assets_subdir = assets_subdir.into();
self
}
pub fn with_scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
pub fn with_asset_id_prefix(mut self, prefix: impl Into<String>) -> Self {
self.asset_id_prefix = prefix.into();
self
}
}
impl Pass for RasterizeSvgToLocalAssetPass {
fn name(&self) -> &'static str {
"rasterize_svg_to_local_asset"
}
fn run(&mut self, doc: &mut Document, ctx: &mut PassCtx) -> Result<()> {
if self.scale <= 0.0 {
bail!("rasterize_svg_to_local_asset scale must be > 0");
}
rasterize_document(
doc,
ctx,
RasterizeConfig {
pass_name: self.name(),
scale: self.scale,
asset_id_prefix: &self.asset_id_prefix,
target: RasterizeTarget::LocalFile {
output_dir: self.output_dir.clone(),
slug: self.slug.clone(),
assets_subdir: self.assets_subdir.clone(),
},
sidecar_key: Some(SIDECAR_GENERATED_RENDER_ASSETS),
},
)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use std::path::Path;
use serde_json::Value as JsonValue;
use tempfile::TempDir;
use typub_ir::{Asset, AssetSource};
use crate::shared::test_fixtures::fixture_doc_for_render_pass;
use crate::{Pass, PassCtx};
use super::{RasterizeSvgToLocalAssetPass, SIDECAR_GENERATED_RENDER_ASSETS};
#[test]
fn rasterize_svg_to_local_asset_pass_writes_png_and_tracks_sidecar() {
let mut doc = fixture_doc_for_render_pass();
let temp_dir = TempDir::new().expect("create temp dir");
let output_dir = temp_dir.path().to_path_buf();
let mut pass = RasterizeSvgToLocalAssetPass::new(output_dir, "hello world")
.with_assets_subdir("assets");
let mut ctx = PassCtx::default();
pass.run(&mut doc, &mut ctx).expect("run local-asset pass");
assert_eq!(doc.assets.len(), 3);
let sidecar = ctx
.sidecar
.get(SIDECAR_GENERATED_RENDER_ASSETS)
.and_then(JsonValue::as_array)
.expect("render sidecar array");
assert_eq!(sidecar.len(), 3);
for entry in sidecar {
let logical = entry
.get("logical_path")
.and_then(JsonValue::as_str)
.expect("logical path");
assert!(logical.starts_with("assets/hello-world-render-"));
let file_path = entry
.get("file_path")
.and_then(JsonValue::as_str)
.expect("file path");
assert!(Path::new(file_path).exists(), "rendered png should exist");
}
for asset in doc.assets.values() {
let Asset::Image(image) = asset else {
panic!("expected image asset");
};
match &image.source {
AssetSource::LocalPath { path } => {
assert!(path.as_str().starts_with("assets/hello-world-render-"))
}
other => panic!("expected local-path source, got: {other:?}"),
}
}
}
}