use std::path::Path;
use std::sync::Arc;
use std::collections::BTreeSet;
use zenith_core::{
AssetKind, BytesAssetProvider, BytesFontProvider, Diagnostic, Document, FontProvider,
FontSource, FontStyle, ImageNode, Node, TokenLiteral, TokenType, TokenValue, default_provider,
dim_to_px,
};
use crate::commands::fonts::os_font_dirs;
use super::entry::RenderCmdErr;
use super::pipeline::verify_locked_sha256;
pub(crate) fn build_font_provider(
doc: &Document,
project_dir: Option<&Path>,
locked: bool,
) -> Result<BytesFontProvider, RenderCmdErr> {
let mut provider = default_provider();
if let Some(dir) = project_dir {
register_project_fonts(&mut provider, doc, dir, locked)?;
}
register_local_fonts(&mut provider, doc);
Ok(provider)
}
fn register_project_fonts(
provider: &mut BytesFontProvider,
doc: &Document,
dir: &Path,
locked: bool,
) -> Result<(), RenderCmdErr> {
for decl in &doc.assets.assets {
if decl.kind != AssetKind::Font {
continue;
}
let path = dir.join(&decl.src);
let bytes = match std::fs::read(&path) {
Ok(b) => b,
Err(e) => {
if locked {
return Err(RenderCmdErr::new(
format!(
"--locked: could not read font asset '{}' from '{}': {}",
decl.id,
path.display(),
e
),
2,
));
}
continue;
}
};
if locked {
verify_locked_sha256(&decl.id, "font asset", decl.sha256.as_deref(), &bytes)?;
}
let arc: Arc<[u8]> = Arc::from(bytes.as_slice());
match zenith_layout::face_metadata(&arc, 0) {
Ok(m) => {
provider.register(&m.family, m.weight, m.style, arc, 0, FontSource::Project);
}
Err(e) => {
if locked {
return Err(RenderCmdErr::new(
format!(
"--locked: font asset '{}' could not be parsed: {}",
decl.id, e
),
2,
));
}
eprintln!(
"warning: font asset '{}' could not be parsed: {} — skipping",
decl.id, e
);
}
}
}
Ok(())
}
fn register_local_fonts(provider: &mut BytesFontProvider, doc: &Document) {
let wanted: BTreeSet<String> = doc
.tokens
.tokens
.iter()
.filter(|t| t.token_type == TokenType::FontFamily)
.filter_map(|t| match &t.value {
TokenValue::Literal(TokenLiteral::String(s)) => Some(s.clone()),
_ => None,
})
.collect();
let needs_scan = wanted.iter().any(|fam| {
provider
.resolve(std::slice::from_ref(fam), 400, FontStyle::Normal)
.is_none()
});
if !needs_scan {
return;
}
for entry in zenith_core::scan_font_dirs(&os_font_dirs()) {
if provider
.resolve(
std::slice::from_ref(&entry.family),
entry.weight,
entry.style,
)
.is_some()
{
continue;
}
let bytes = match std::fs::read(&entry.path) {
Ok(b) => b,
Err(_) => continue,
};
let arc: Arc<[u8]> = Arc::from(bytes.as_slice());
provider.register(
&entry.family,
entry.weight,
entry.style,
arc,
entry.index,
FontSource::Local,
);
}
}
pub(crate) fn build_asset_provider(
doc: &Document,
project_dir: &Path,
locked: bool,
) -> Result<BytesAssetProvider, RenderCmdErr> {
let mut provider = BytesAssetProvider::new();
for decl in &doc.assets.assets {
if !matches!(decl.kind, AssetKind::Image | AssetKind::Svg) {
continue;
}
let path = project_dir.join(&decl.src);
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(e) => {
if locked {
return Err(RenderCmdErr::new(
format!(
"--locked: could not read asset '{}' from '{}': {}",
decl.id,
path.display(),
e
),
2,
));
}
continue;
}
};
if locked {
verify_locked_sha256(&decl.id, "asset", decl.sha256.as_deref(), &bytes)?;
}
provider.register(&decl.id, decl.kind.clone(), bytes.into());
}
Ok(provider)
}
pub(crate) fn collect_missing_asset_diagnostics(
doc: &Document,
project_dir: &Path,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for decl in &doc.assets.assets {
let path = project_dir.join(&decl.src);
if !path.exists() {
diagnostics.push(Diagnostic::error(
"asset.missing",
format!("asset '{}' file not found: '{}'", decl.id, path.display()),
decl.source_span,
Some(decl.id.clone()),
));
}
}
diagnostics
}
pub fn collect_image_dimension_diagnostics(doc: &Document, project_dir: &Path) -> Vec<Diagnostic> {
let mut out = Vec::new();
for page in &doc.body.pages {
walk_images(&page.children, doc, project_dir, &mut out);
}
out
}
pub(super) fn disk_diagnostics(doc: &Document, project_dir: Option<&Path>) -> Vec<Diagnostic> {
match project_dir {
Some(dir) => {
let mut d = collect_missing_asset_diagnostics(doc, dir);
d.extend(collect_image_dimension_diagnostics(doc, dir));
d
}
None => Vec::new(),
}
}
fn walk_images(nodes: &[Node], doc: &Document, project_dir: &Path, out: &mut Vec<Diagnostic>) {
for node in nodes {
match node {
Node::Image(img) => {
check_image(img, doc, project_dir, out);
}
Node::Frame(f) => {
walk_images(&f.children, doc, project_dir, out);
}
Node::Group(g) => {
walk_images(&g.children, doc, project_dir, out);
}
Node::Table(t) => {
for row in &t.rows {
for cell in &row.cells {
walk_images(&cell.children, doc, project_dir, out);
}
}
}
Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Toc(_)
| Node::Footnote(_)
| Node::Shape(_)
| Node::Connector(_)
| Node::Pattern(_)
| Node::Chart(_)
| Node::Light(_)
| Node::Mesh(_)
| Node::Unknown(_) => {}
}
}
}
fn check_image(img: &ImageNode, doc: &Document, project_dir: &Path, out: &mut Vec<Diagnostic>) {
let w_dim = match img.w.as_ref() {
Some(zenith_core::PropertyValue::Dimension(d)) => d,
_ => return,
};
let h_dim = match img.h.as_ref() {
Some(zenith_core::PropertyValue::Dimension(d)) => d,
_ => return,
};
let w = match dim_to_px(w_dim.value, &w_dim.unit) {
Some(px) => px,
None => return,
};
let h = match dim_to_px(h_dim.value, &h_dim.unit) {
Some(px) => px,
None => return,
};
let decl = match doc.assets.assets.iter().find(|d| d.id == img.asset) {
Some(d) => d,
None => return,
};
if decl.kind != AssetKind::Image {
return;
}
let path = project_dir.join(&decl.src);
let isz = match imagesize::size(&path) {
Ok(s) => s,
Err(_) => return, };
let iw = isz.width as f64;
let ih = isz.height as f64;
let fit = img.fit.as_deref();
if fit == Some("none") && (iw > w || ih > h) {
out.push(Diagnostic::advisory(
"image.overflow",
format!(
"image '{}': intrinsic size {}x{} exceeds its box {}x{} (fit=\"none\")",
img.id, iw as u32, ih as u32, w as u32, h as u32,
),
img.source_span,
Some(img.id.clone()),
));
}
let upscales = match fit {
Some("none") => false,
Some("stretch") | None => w > iw || h > ih,
Some("contain") => {
let s = (w / iw).min(h / ih);
s > 1.0
}
Some("cover") => {
let s = (w / iw).max(h / ih);
s > 1.0
}
Some(_) => false, };
if upscales {
out.push(Diagnostic::advisory(
"image.upscale",
format!(
"image '{}': rendered larger than its intrinsic {}x{} px; raster will appear pixelated",
img.id,
iw as u32,
ih as u32,
),
img.source_span,
Some(img.id.clone()),
));
}
}