Skip to main content

native/skia_native/src/lib.rs

#![allow(deprecated)]

use rustler::{Atom, Binary, Encoder, Env, NifResult, OwnedBinary, ResourceArc, Term};
use rustler::types::map::map_new;
use skia_safe::{
    canvas::SaveLayerRec,
    font_style::{Slant, Weight, Width},
    vertices::VertexMode,
    image_filters, path_utils, surfaces,
    textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle, TextAlign, TextDirection, TextStyle},
    color_filters, AlphaType, Color, ColorFilter, ColorType, CubicResampler, Data,
    runtime_effect::ChildPtr,
    ClipOp, EncodedImageFormat, FilterMode, Font, FontMgr, FontStyle, IPoint, Image, ImageInfo, MaskFilter, Matrix,
    Paint, PaintStyle, PathBuilder, PathDirection, PathEffect, Picture, PictureRecorder, Point, RRect,
    Rect, RuntimeEffect, SamplingOptions, Shader, TextBlob, TileMode, Vertices,
};

include!("generated_resources.rs");

mod atoms {
    include!("generated_atoms.rs");
}

mod generated_enums;
mod generated_opts;

include!("generated_nifs.rs");

fn compile_runtime_effect_impl<'a>(env: Env<'a>, source: String) -> NifResult<Term<'a>> {
    match RuntimeEffect::make_for_shader(&source, None) {
        Ok(_) => Ok((atoms::ok(), ResourceArc::new(EncodedRuntimeEffect { source })).encode(env)),
        Err(error) => Ok((atoms::error(), error).encode(env)),
    }
}

fn render_png_impl<'a>(env: Env<'a>, batch: Term<'a>) -> NifResult<Term<'a>> {
    encode_rendered(env, batch, EncodedImageFormat::PNG, 100)
}

fn render_jpeg_impl<'a>(env: Env<'a>, batch: Term<'a>, quality: u32) -> NifResult<Term<'a>> {
    encode_rendered(env, batch, EncodedImageFormat::JPEG, quality)
}

fn render_webp_impl<'a>(env: Env<'a>, batch: Term<'a>, quality: u32) -> NifResult<Term<'a>> {
    encode_rendered(env, batch, EncodedImageFormat::WEBP, quality)
}

fn render_rgba_impl<'a>(env: Env<'a>, batch: Term<'a>) -> NifResult<Term<'a>> {
    encode_rgba_surface(env, batch, render_surface)
}

fn render_compact_png_impl<'a>(env: Env<'a>, batch: Term<'a>) -> NifResult<Term<'a>> {
    let mut surface = match render_compact_surface(env, batch)? {
        Ok(surface) => surface,
        Err(reason) => return Ok((atoms::error(), reason).encode(env)),
    };

    let image = surface.image_snapshot();
    let data = match image.encode(None, EncodedImageFormat::PNG, 100) {
        Some(data) => data,
        None => return Ok((atoms::error(), atoms::unsupported_format()).encode(env)),
    };

    Ok((atoms::ok(), binary(env, data.as_bytes())?).encode(env))
}

fn render_compact_rgba_impl<'a>(env: Env<'a>, batch: Term<'a>) -> NifResult<Term<'a>> {
    encode_rgba_surface(env, batch, |batch| render_compact_surface(env, batch))
}

fn encode_rgba_surface<'a, F>(
    env: Env<'a>,
    batch: Term<'a>,
    render: F,
) -> NifResult<Term<'a>>
where
    F: FnOnce(Term<'a>) -> NifResult<Result<skia_safe::Surface, Atom>>,
{
    let width = batch_width(batch)?;
    let height = batch_height(batch)?;
    let mut surface = match render(batch)? {
        Ok(surface) => surface,
        Err(reason) => return Ok((atoms::error(), reason).encode(env)),
    };

    let stride = width as usize * 4;
    let mut pixels = vec![0_u8; stride * height as usize];
    let image_info = ImageInfo::new(
        (width, height),
        ColorType::RGBA8888,
        AlphaType::Premul,
        None,
    );

    if !surface.read_pixels(&image_info, &mut pixels, stride, IPoint::new(0, 0)) {
        return Ok((atoms::error(), atoms::render_failed()).encode(env));
    }

    Ok((
        atoms::ok(),
        (width, height, stride as i64, binary(env, &pixels)?),
    )
        .encode(env))
}

fn decode_image_impl<'a>(env: Env<'a>, bytes: Binary<'a>) -> NifResult<Term<'a>> {
    let data = Data::new_copy(bytes.as_slice());
    let image = match Image::from_encoded(data) {
        Some(image) => image,
        None => return Ok((atoms::error(), atoms::invalid_image()).encode(env)),
    };

    let width = image.width();
    let height = image.height();
    let resource = ResourceArc::new(EncodedImage { image });

    Ok((atoms::ok(), resource, width, height).encode(env))
}

fn encode_image_impl<'a>(
    env: Env<'a>,
    image_term: Term<'a>,
    format: Atom,
    quality: u32,
) -> NifResult<Term<'a>> {
    let image = match image_from_term(image_term) {
        Ok(image) => image,
        Err(_) => return Ok((atoms::error(), atoms::invalid_image()).encode(env)),
    };
    let format = match generated_enums::decode_encoded_image_format(format) {
        Ok(format) => format,
        Err(_) => return Ok((atoms::error(), atoms::unsupported_format()).encode(env)),
    };

    match image.encode(None, format, quality.clamp(0, 100)) {
        Some(data) => Ok((atoms::ok(), binary(env, data.as_bytes())?).encode(env)),
        None => Ok((atoms::error(), atoms::unsupported_format()).encode(env)),
    }
}

fn resize_image_impl<'a>(
    env: Env<'a>,
    image_term: Term<'a>,
    width: i32,
    height: i32,
) -> NifResult<Term<'a>> {
    if width <= 0 || height <= 0 {
        return Ok((atoms::error(), atoms::invalid_image()).encode(env));
    }
    let image = match image_from_term(image_term) {
        Ok(image) => image,
        Err(_) => return Ok((atoms::error(), atoms::invalid_image()).encode(env)),
    };
    let encoded = render_image_resource(
        &image,
        None,
        Rect::from_xywh(0.0, 0.0, width as f32, height as f32),
    )?;
    let image = Image::from_encoded(Data::new_copy(&encoded)).ok_or(rustler::Error::BadArg)?;
    Ok((
        atoms::ok(),
        ResourceArc::new(EncodedImage { image }),
    )
        .encode(env))
}

fn crop_image_impl<'a>(
    env: Env<'a>,
    image_term: Term<'a>,
    source: (f64, f64, f64, f64),
) -> NifResult<Term<'a>> {
    let image = match image_from_term(image_term) {
        Ok(image) => image,
        Err(_) => return Ok((atoms::error(), atoms::invalid_image()).encode(env)),
    };
    let src = Rect::from_xywh(
        source.0 as f32,
        source.1 as f32,
        source.2 as f32,
        source.3 as f32,
    );
    let encoded = render_image_resource(
        &image,
        Some(src),
        Rect::from_xywh(0.0, 0.0, src.width(), src.height()),
    )?;
    let image = Image::from_encoded(Data::new_copy(&encoded)).ok_or(rustler::Error::BadArg)?;
    Ok((
        atoms::ok(),
        ResourceArc::new(EncodedImage { image }),
    )
        .encode(env))
}

fn load_font_impl<'a>(env: Env<'a>, bytes: Binary<'a>) -> NifResult<Term<'a>> {
    let Some(typeface) = FontMgr::new().new_from_data(bytes.as_slice(), None) else {
        return Ok((atoms::error(), atoms::invalid_font()).encode(env));
    };

    Ok((
        atoms::ok(),
        ResourceArc::new(EncodedFont { typeface }),
    )
        .encode(env))
}

fn font_families_impl<'a>(env: Env<'a>) -> NifResult<Term<'a>> {
    let families: Vec<String> = FontMgr::new().family_names().collect();
    Ok((atoms::ok(), families).encode(env))
}

fn match_font_impl<'a>(env: Env<'a>, family: String, weight: i32, slant: Atom) -> NifResult<Term<'a>> {
    let slant = if slant == atoms::italic() {
        Slant::Italic
    } else if slant == atoms::oblique() {
        Slant::Oblique
    } else {
        Slant::Upright
    };
    let style = FontStyle::new(Weight::from(weight), Width::NORMAL, slant);
    let Some(typeface) = FontMgr::new().match_family_style(family, style) else {
        return Ok((atoms::error(), atoms::invalid_font()).encode(env));
    };

    Ok((
        atoms::ok(),
        ResourceArc::new(EncodedFont { typeface }),
    )
        .encode(env))
}

fn typeface_info_impl<'a>(env: Env<'a>, typeface_term: Term<'a>) -> NifResult<Term<'a>> {
    let typeface_ref = match decode_encoded_font_ref(typeface_term) {
        Ok(typeface_ref) => typeface_ref,
        Err(_) => return Ok((atoms::error(), atoms::invalid_font()).encode(env)),
    };
    let typeface = &typeface_ref.typeface;
    let style = typeface.font_style();
    let slant = match style.slant() {
        Slant::Italic => atoms::italic(),
        Slant::Oblique => atoms::oblique(),
        _ => atoms::normal(),
    };

    Ok((
        atoms::ok(),
        (
            typeface.unique_id() as i64,
            *style.weight() as i64,
            *style.width() as i64,
            slant,
            typeface.is_bold(),
            typeface.is_italic(),
            typeface.is_fixed_pitch(),
        ),
    )
        .encode(env))
}

fn font_metrics_impl<'a>(env: Env<'a>, font_term: Term<'a>) -> NifResult<Term<'a>> {
    let font = match font_from_term(font_term, 16.0) {
        Ok(font) => font,
        Err(_) => return Ok((atoms::error(), atoms::invalid_font()).encode(env)),
    };
    let (line_spacing, metrics) = font.metrics();

    Ok((
        atoms::ok(),
        vec![
            line_spacing,
            metrics.top,
            metrics.ascent,
            metrics.descent,
            metrics.bottom,
            metrics.leading,
            metrics.avg_char_width,
            metrics.max_char_width,
            metrics.x_min,
            metrics.x_max,
            metrics.x_height,
            metrics.cap_height,
        ],
    )
        .encode(env))
}

fn font_glyph_ids_impl<'a>(env: Env<'a>, font_term: Term<'a>, text: String) -> NifResult<Term<'a>> {
    let font = match font_from_term(font_term, 16.0) {
        Ok(font) => font,
        Err(_) => return Ok((atoms::error(), atoms::invalid_font()).encode(env)),
    };
    let glyphs: Vec<u16> = font.str_to_glyphs_vec(text).into_iter().collect();
    Ok((atoms::ok(), glyphs).encode(env))
}

fn measure_text_impl<'a>(
    env: Env<'a>,
    text: String,
    font_term: Term<'a>,
    size: f64,
) -> NifResult<Term<'a>> {
    let font = match font_from_term(font_term, size as f32) {
        Ok(font) => font,
        Err(_) => return Ok((atoms::error(), atoms::invalid_font()).encode(env)),
    };
    let paint = Paint::default();
    let (width, bounds) = font.measure_str(text, Some(&paint));

    Ok((
        atoms::ok(),
        width,
        bounds.left,
        bounds.top,
        bounds.right,
        bounds.bottom,
    )
        .encode(env))
}

fn create_text_blob_impl<'a>(
    env: Env<'a>,
    text: String,
    font_term: Term<'a>,
    size: f64,
) -> NifResult<Term<'a>> {
    let font = match font_from_term(font_term, size as f32) {
        Ok(font) => font,
        Err(_) => return Ok((atoms::error(), atoms::invalid_font()).encode(env)),
    };
    let Some(blob) = TextBlob::from_str(text, &font) else {
        return Ok((atoms::error(), atoms::invalid_text_blob()).encode(env));
    };

    Ok((atoms::ok(), ResourceArc::new(EncodedTextBlob { blob })).encode(env))
}

fn text_blob_bounds_impl<'a>(env: Env<'a>, blob_term: Term<'a>) -> NifResult<Term<'a>> {
    let blob = match text_blob_from_term(blob_term) {
        Ok(blob) => blob,
        Err(_) => return Ok((atoms::error(), atoms::invalid_text_blob()).encode(env)),
    };
    let bounds = blob.bounds();
    Ok((atoms::ok(), (bounds.left, bounds.top, bounds.right, bounds.bottom)).encode(env))
}

fn path_to_svg_impl<'a>(env: Env<'a>, path_term: Term<'a>) -> NifResult<Term<'a>> {
    match build_path(path_term) {
        Ok(path) => Ok((atoms::ok(), path.to_svg()).encode(env)),
        Err(_) => Ok((atoms::error(), atoms::invalid_path()).encode(env)),
    }
}

fn record_picture_impl<'a>(env: Env<'a>, batch: Term<'a>) -> NifResult<Term<'a>> {
    let width = batch.map_get(atoms::width())?.decode::<i32>()?;
    let height = batch.map_get(atoms::height())?.decode::<i32>()?;

    if width <= 0 || height <= 0 {
        return Ok((atoms::error(), atoms::invalid_batch()).encode(env));
    }

    let mut recorder = PictureRecorder::new();
    {
        let canvas = recorder.begin_recording(Rect::from_xywh(0.0, 0.0, width as f32, height as f32), false);
        canvas.clear(Color::TRANSPARENT);

        for command in batch.map_get(atoms::commands())?.decode::<Vec<Term>>()? {
            if let Err(reason) = draw_command_result(canvas, command)? {
                return Ok((atoms::error(), reason).encode(env));
            }
        }
    }

    let Some(picture) = recorder.finish_recording_as_picture(None) else {
        return Ok((atoms::error(), atoms::render_failed()).encode(env));
    };

    Ok((
        atoms::ok(),
        ResourceArc::new(EncodedPicture {
            bytes: picture.serialize().as_bytes().to_vec(),
            picture,
        }),
    )
        .encode(env))
}

fn decode_picture_impl<'a>(env: Env<'a>, bytes: Binary<'a>) -> NifResult<Term<'a>> {
    let Some(picture) = Picture::from_bytes(bytes.as_slice()) else {
        return Ok((atoms::error(), atoms::invalid_picture()).encode(env));
    };

    Ok((
        atoms::ok(),
        ResourceArc::new(EncodedPicture {
            bytes: bytes.as_slice().to_vec(),
            picture,
        }),
    )
        .encode(env))
}

fn encode_picture_impl<'a>(env: Env<'a>, picture_term: Term<'a>) -> NifResult<Term<'a>> {
    let picture_ref = match decode_encoded_picture_ref(picture_term) {
        Ok(picture_ref) => picture_ref,
        Err(_) => return Ok((atoms::error(), atoms::invalid_picture()).encode(env)),
    };
    Ok((atoms::ok(), binary(env, &picture_ref.bytes)?).encode(env))
}

fn picture_info_impl<'a>(env: Env<'a>, picture_term: Term<'a>) -> NifResult<Term<'a>> {
    let picture = match picture_from_term(picture_term) {
        Ok(picture) => picture,
        Err(_) => return Ok((atoms::error(), atoms::invalid_picture()).encode(env)),
    };
    let cull = picture.cull_rect();

    Ok((
        atoms::ok(),
        (
            cull.left,
            cull.top,
            cull.right,
            cull.bottom,
            picture.approximate_op_count() as i64,
            picture.approximate_op_count_nested(true) as i64,
            picture.approximate_bytes_used() as i64,
        ),
    )
        .encode(env))
}

fn image_from_term(image_term: Term) -> NifResult<Image> {
    let image_ref = decode_encoded_image_ref(image_term)?;
    Ok(image_ref.image.clone())
}

fn picture_from_term(picture_term: Term) -> NifResult<Picture> {
    let picture_ref = decode_encoded_picture_ref(picture_term)?;
    Ok(picture_ref.picture.clone())
}

fn text_blob_from_term(blob_term: Term) -> NifResult<TextBlob> {
    let blob_ref = decode_encoded_text_blob_ref(blob_term)?;
    Ok(blob_ref.blob.clone())
}

fn runtime_effect_from_term(effect_term: Term) -> NifResult<RuntimeEffect> {
    let effect_ref = decode_encoded_runtime_effect_ref(effect_term)?;
    RuntimeEffect::make_for_shader(&effect_ref.source, None).map_err(|_| rustler::Error::BadArg)
}

fn vertices_from_term(term: Term) -> NifResult<Vertices> {
    let (mode, positions, colors, indices_term) = term.decode::<(Atom, Vec<(f64, f64)>, Vec<Term>, Term)>()?;
    let mode = if mode == atoms::triangle_strip() {
        VertexMode::TriangleStrip
    } else if mode == atoms::triangle_fan() {
        VertexMode::TriangleFan
    } else {
        VertexMode::Triangles
    };
    let positions: Vec<Point> = positions
        .into_iter()
        .map(|(x, y)| Point::new(x as f32, y as f32))
        .collect();
    let colors: Vec<Color> = colors
        .into_iter()
        .map(decode_color)
        .collect::<NifResult<Vec<Color>>>()?;
    let texs = positions.clone();
    let indices = if indices_term.decode::<Atom>().is_ok_and(|atom| atom == atoms::nil()) {
        None
    } else {
        Some(indices_term.decode::<Vec<u16>>()?)
    };

    Ok(Vertices::new_copy(mode, &positions, &texs, &colors, indices.as_deref()))
}

fn render_image_resource(image: &Image, src: Option<Rect>, dst: Rect) -> NifResult<Vec<u8>> {
    let width = dst.width().round() as i32;
    let height = dst.height().round() as i32;
    let mut surface = surfaces::raster_n32_premul((width, height)).ok_or(rustler::Error::BadArg)?;
    surface.canvas().clear(Color::TRANSPARENT);
    let paint = Paint::default();
    let source = src
        .as_ref()
        .map(|rect| (rect, skia_safe::canvas::SrcRectConstraint::Strict));
    surface.canvas().draw_image_rect_with_sampling_options(
        image,
        source,
        dst,
        SamplingOptions::from(FilterMode::Linear),
        &paint,
    );
    let snapshot = surface.image_snapshot();
    let data = snapshot
        .encode(None, EncodedImageFormat::PNG, 100)
        .ok_or(rustler::Error::BadArg)?;
    Ok(data.as_bytes().to_vec())
}

fn encode_rendered<'a>(
    env: Env<'a>,
    batch: Term<'a>,
    format: EncodedImageFormat,
    quality: u32,
) -> NifResult<Term<'a>> {
    let mut surface = match render_surface(batch)? {
        Ok(surface) => surface,
        Err(reason) => return Ok((atoms::error(), reason).encode(env)),
    };

    let image = surface.image_snapshot();
    let data = match image.encode(None, format, quality.clamp(0, 100)) {
        Some(data) => data,
        None => return Ok((atoms::error(), atoms::unsupported_format()).encode(env)),
    };

    Ok((atoms::ok(), binary(env, data.as_bytes())?).encode(env))
}

fn render_surface(batch: Term) -> NifResult<Result<skia_safe::Surface, Atom>> {
    let width = batch_width(batch)?;
    let height = batch_height(batch)?;

    let mut surface = match new_surface(width, height) {
        Ok(surface) => surface,
        Err(reason) => return Ok(Err(reason)),
    };

    for command in batch.map_get(atoms::commands())?.decode::<Vec<Term>>()? {
        if let Err(reason) = draw_command_result(surface.canvas(), command)? {
            return Ok(Err(reason));
        }
    }

    Ok(Ok(surface))
}

fn render_compact_surface<'a>(env: Env<'a>, batch: Term<'a>) -> NifResult<Result<skia_safe::Surface, Atom>> {
    let width = batch_width(batch)?;
    let height = batch_height(batch)?;

    let mut surface = match new_surface(width, height) {
        Ok(surface) => surface,
        Err(reason) => return Ok(Err(reason)),
    };

    let (_, _, commands) = batch.decode::<(i32, i32, Vec<Term>)>()?;
    for command in commands {
        let command = command_from_compact(env, command)?;
        if let Err(reason) = draw_command_result(surface.canvas(), command)? {
            return Ok(Err(reason));
        }
    }

    Ok(Ok(surface))
}

fn new_surface(width: i32, height: i32) -> Result<skia_safe::Surface, Atom> {
    if width <= 0 || height <= 0 {
        return Err(atoms::invalid_batch());
    }

    let mut surface = surfaces::raster_n32_premul((width, height)).ok_or(atoms::render_failed())?;
    surface.canvas().clear(Color::TRANSPARENT);
    Ok(surface)
}

fn batch_width(batch: Term) -> NifResult<i32> {
    if let Ok(term) = batch.map_get(atoms::width()) {
        if let Ok(width) = term.decode::<i32>() {
            return Ok(width);
        }
    }
    let (width, _, _) = batch.decode::<(i32, i32, Vec<Term>)>()?;
    Ok(width)
}

fn batch_height(batch: Term) -> NifResult<i32> {
    if let Ok(term) = batch.map_get(atoms::height()) {
        if let Ok(height) = term.decode::<i32>() {
            return Ok(height);
        }
    }
    let (_, height, _) = batch.decode::<(i32, i32, Vec<Term>)>()?;
    Ok(height)
}

fn command_from_compact<'a>(env: Env<'a>, command: Term<'a>) -> NifResult<Term<'a>> {
    let (op_id, args, opts) = command.decode::<(i64, Vec<Term>, Vec<(Atom, Term)>)>()?;
    map_new(env)
        .map_put(atoms::op(), compact_op_atom(op_id)?)?
        .map_put(atoms::args(), args)?
        .map_put(atoms::opts(), opts)
}

include!("generated_dispatch.rs");

fn draw_command_result(canvas: &skia_safe::Canvas, command: Term) -> NifResult<Result<(), Atom>> {
    match draw_command(canvas, command) {
        Ok(()) => Ok(Ok(())),
        Err(_) => Ok(Err(atoms::invalid_command())),
    }
}

include!("generated_handlers.rs");
include!("generated_style_helpers.rs");
include!("generated_layers.rs");
include!("generated_transforms.rs");
include!("generated_shapes.rs");
include!("generated_text.rs");
include!("generated_images.rs");
include!("generated_draw_paths.rs");
include!("generated_clips.rs");
include!("generated_path.rs");
include!("generated_paint.rs");

fn fill_paint(color: Color) -> Paint {
    let mut paint = Paint::default();
    paint
        .set_anti_alias(true)
        .set_style(PaintStyle::Fill)
        .set_color(color);
    paint
}

fn stroke_paint(color: Color, width: f32, opts: &[(Atom, Term)]) -> NifResult<Paint> {
    let mut paint = Paint::default();
    paint
        .set_anti_alias(true)
        .set_style(PaintStyle::Stroke)
        .set_stroke_width(width)
        .set_color(color);
    apply_stroke_options(&mut paint, opts)?;
    apply_blend_mode(&mut paint, opts)?;
    Ok(paint)
}

fn binary<'a>(env: Env<'a>, bytes: &[u8]) -> NifResult<Binary<'a>> {
    let mut owned =
        OwnedBinary::new(bytes.len()).ok_or(rustler::Error::Term(Box::new("alloc_failed")))?;
    owned.as_mut_slice().copy_from_slice(bytes);
    Ok(Binary::from_owned(owned, env))
}

include!("generated_opts_helpers.rs");

fn point_from_term(term: Term) -> NifResult<Point> {
    let (x, y) = term.decode::<(f64, f64)>()?;
    Ok(Point::new(x as f32, y as f32))
}

fn matrix_from_term(term: Term) -> NifResult<Matrix> {
    let (m00, m01, m02, m10, m11, m12) = term.decode::<(f64, f64, f64, f64, f64, f64)>()?;
    Ok(Matrix::new_all(
        m00 as f32, m01 as f32, m02 as f32, m10 as f32, m11 as f32, m12 as f32, 0.0, 0.0, 1.0,
    ))
}

fn rect_from_term(term: Term) -> NifResult<Rect> {
    let (x, y, width, height) = term.decode::<(f64, f64, f64, f64)>()?;
    Ok(Rect::from_xywh(
        x as f32,
        y as f32,
        width as f32,
        height as f32,
    ))
}

fn opt_sampling<'a>(opts: &[(Atom, Term<'a>)], key: Atom) -> NifResult<SamplingOptions> {
    match opt_term(opts, key) {
        Some(term) => decode_sampling_options(term),
        None => Ok(SamplingOptions::default()),
    }
}

fn opt_fill_paint<'a>(opts: &[(Atom, Term<'a>)], key: Atom) -> NifResult<Option<Paint>> {
    match opt_term(opts, key) {
        Some(term) => Ok(Some(decode_paint(term)?)),
        None => Ok(None),
    }
}

fn opt_color<'a>(opts: &[(Atom, Term<'a>)], key: Atom) -> NifResult<Option<Color>> {
    match opt_term(opts, key) {
        Some(term) => Ok(Some(decode_color(term)?)),
        None => Ok(None),
    }
}

fn font_from_term(term: Term, size: f32) -> NifResult<Font> {
    if term.decode::<Atom>().is_ok_and(|atom| atom == atoms::nil()) {
        let typeface = FontMgr::new()
            .legacy_make_typeface(None, FontStyle::normal())
            .ok_or(rustler::Error::BadArg)?;
        return Ok(Font::new(typeface, size));
    }

    if let Ok(typeface_term) = term.map_get(atoms::typeface()) {
        let font_size = match term.map_get(atoms::size()) {
            Ok(size_term) => {
                if size_term.decode::<Atom>().is_ok_and(|atom| atom == atoms::nil()) {
                    size
                } else {
                    size_term.decode::<f64>()? as f32
                }
            }
            Err(_) => size,
        };

        if typeface_term.decode::<Atom>().is_ok_and(|atom| atom == atoms::nil()) {
            let typeface = FontMgr::new()
                .legacy_make_typeface(None, FontStyle::normal())
                .ok_or(rustler::Error::BadArg)?;
            return Ok(Font::new(typeface, font_size));
        }

        let typeface_ref = decode_encoded_font_ref(typeface_term)?;
        return Ok(Font::new(typeface_ref.typeface.clone(), font_size));
    }

    let typeface_ref = decode_encoded_font_ref(term)?;
    Ok(Font::new(typeface_ref.typeface.clone(), size))
}

rustler::init!("Elixir.Skia.Native");