rasterlottie 0.2.1

Pure Rust, headless Lottie rasterizer for deterministic server-side rendering
Documentation
#[cfg(feature = "text")]
use unicode_segmentation::UnicodeSegmentation;

use super::analyzer::{SupportProfile, SupportReport, UnsupportedFeature, UnsupportedKind};
#[cfg(feature = "text")]
use crate::model::TextDocument;
use crate::{
    effects::supported_layer_effects,
    model::{Animation, Layer, LayerType},
};

#[cfg(feature = "text")]
pub(super) fn visit_text_layer(
    animation: &Animation,
    layer: &Layer,
    path: &str,
    report: &mut SupportReport,
) {
    let Some(text) = layer.text.as_ref() else {
        report.push(UnsupportedFeature {
            path: path.to_string(),
            kind: UnsupportedKind::Text,
            detail: "text layer data is missing".to_string(),
        });
        return;
    };

    if text.document.keyframes.is_empty() {
        report.push(UnsupportedFeature {
            path: format!("{path}.text"),
            kind: UnsupportedKind::Text,
            detail: "text layer has no document keyframes".to_string(),
        });
    }

    if text.has_animators() {
        report.push(UnsupportedFeature {
            path: format!("{path}.text"),
            kind: UnsupportedKind::Text,
            detail: "text animators are not supported yet".to_string(),
        });
    }

    if text.has_path() {
        report.push(UnsupportedFeature {
            path: format!("{path}.text"),
            kind: UnsupportedKind::Text,
            detail: "text path data is not supported yet".to_string(),
        });
    }

    for (index, keyframe) in text.document.keyframes.iter().enumerate() {
        visit_text_document(
            animation,
            &keyframe.document,
            &format!("{path}.text.keyframes[{index}]"),
            report,
        );
    }
}

#[cfg(not(feature = "text"))]
pub(super) fn visit_text_layer(
    _animation: &Animation,
    _layer: &Layer,
    path: &str,
    report: &mut SupportReport,
) {
    report.push(UnsupportedFeature {
        path: path.to_string(),
        kind: UnsupportedKind::Text,
        detail: "text support is disabled because the `text` feature is not enabled".to_string(),
    });
}

#[cfg(feature = "text")]
fn visit_text_document(
    animation: &Animation,
    document: &TextDocument,
    path: &str,
    report: &mut SupportReport,
) {
    if document.size <= f32::EPSILON {
        report.push(UnsupportedFeature {
            path: path.to_string(),
            kind: UnsupportedKind::Text,
            detail: "text size must be positive".to_string(),
        });
    }

    if document.justify > 2 {
        report.push(UnsupportedFeature {
            path: format!("{path}.justify"),
            kind: UnsupportedKind::Text,
            detail: "text justification mode is not supported yet".to_string(),
        });
    }

    if document.box_size.is_some() {
        report.push(UnsupportedFeature {
            path: format!("{path}.box_size"),
            kind: UnsupportedKind::Text,
            detail: "boxed text is not supported yet".to_string(),
        });
    }

    let Some(font) = animation.lookup_font(&document.font) else {
        report.push(UnsupportedFeature {
            path: format!("{path}.font"),
            kind: UnsupportedKind::Text,
            detail: format!("missing font `{}`", document.font),
        });
        return;
    };

    for grapheme in document.text.graphemes(true) {
        if is_text_newline(grapheme) {
            continue;
        }

        let Some(glyph) = animation.lookup_glyph(grapheme, font) else {
            report.push(UnsupportedFeature {
                path: path.to_string(),
                kind: UnsupportedKind::Text,
                detail: format!("missing glyph data for `{grapheme}`"),
            });
            continue;
        };

        if glyph.size <= f32::EPSILON {
            report.push(UnsupportedFeature {
                path: path.to_string(),
                kind: UnsupportedKind::Text,
                detail: format!("glyph `{grapheme}` has non-positive size"),
            });
        }
    }
}

pub(super) fn layer_effects_are_supported(layer: &Layer, profile: &SupportProfile) -> bool {
    profile.allow_effects
        || layer.layer_type == LayerType::NULL
        || supported_layer_effects(layer).is_some()
}

#[cfg(feature = "text")]
fn is_text_newline(grapheme: &str) -> bool {
    matches!(grapheme, "\r" | "\n" | "\r\n" | "\u{0003}")
}