bevy_pxtxt 0.2.0

Create fonts and render text in bevy based on a bitmap source image.
Documentation
use std::{fs::File, io::BufReader, path::PathBuf};

use ahash::AHashMap;
use bevy::{asset::{Asset, AssetLoader, AsyncReadExt}, math::URect, reflect::TypePath};
use image::{ImageFormat, Rgba, RgbaImage};
use thiserror::Error;

use crate::pxfontdata::{GlyphWidth, PxFontData};

#[derive(Debug, Clone)]
pub(crate) struct PxGlyph {
    pub(crate) src_rect: URect,
}

#[derive(Asset, TypePath)]
pub struct PxFont {
    pub(crate) name: String,
    pub(crate) source: RgbaImage,
    pub(crate) char_map: AHashMap<char, PxGlyph>,
    pub(crate) ascender: u32,
    pub(crate) descender: u32,
    pub(crate) spacing: u32,
}

#[derive(Debug, Error)]
pub enum PxFontLoadError {
    #[error("An error was encountered parsing the image: {0}")]
    Image(#[from] image::ImageError),
    #[error("An error was encountered loading the PxFontData file: {0}")]
    Io(#[from] std::io::Error),
    #[error("An error was encountered parsing the RON file: {0}")]
    Ron(#[from] ron::error::SpannedError),
    #[error("Unsupported image type {0}")]
    UnsupportedExtension(String),
    #[error("The image file has no extension, so the format cannot be determined.")]
    NoExtensionProvided,
    #[error("The ascender must not be zero.")]
    MissingAscender,
    #[error("The descender must not be zero.")]
    MissingDescender,
}

#[derive(Default)]
pub struct PxFontLoader;

impl AssetLoader for PxFontLoader {
    type Asset = PxFont;
    type Settings = ();
    type Error = PxFontLoadError;

    fn load<'a>(
            &'a self,
            reader: &'a mut bevy::asset::io::Reader,
            _settings: &'a Self::Settings,
            _load_context: &'a mut bevy::asset::LoadContext,
        ) -> impl bevy::utils::ConditionalSendFuture<Output = Result<Self::Asset, Self::Error>> {
        Box::pin(async move {
            let mut bytes = Vec::new();
            reader.read_to_end(&mut bytes).await?;
            let data = ron::de::from_bytes::<PxFontData>(&bytes)?;

            if data.ascender == 0 {
                return Err(PxFontLoadError::MissingAscender);
            }

            if data.descender == 0 {
                return Err(PxFontLoadError::MissingDescender);
            }

            let format = match data.image.extension() {
                #[cfg(feature = "png")]
                Some(ext) if ext == "png" => ImageFormat::Png,
                #[cfg(feature = "jpeg")]
                Some(ext) if ext == "jpeg" || ext == "jpg" => ImageFormat::Jpeg,
                #[cfg(feature = "gif")]
                Some(ext) if ext == "gif" => ImageFormat::Gif,
                #[cfg(feature = "tiff")]
                Some(ext) if ext == "tiff" || ext == "tif" => ImageFormat::Tiff,
                Some(ext) => return Err(PxFontLoadError::UnsupportedExtension(
                    ext.to_str().unwrap().to_string()
                )),
                None => return Err(PxFontLoadError::NoExtensionProvided),
            };
            let file = File::open(PathBuf::from("assets").join(data.image))?;
            let mut rdr = BufReader::new(file);
            let image = image::load(&mut rdr, format)?;
            let source = image.into_rgba8();

            let mut char_map = AHashMap::new();
            let mut x = 0;
            let mut y = 0;

            'map_chars: for c in data.char_layout {
                let height = data.ascender + data.descender;
                let width = match data.glyph_width {
                    GlyphWidth::Varied { max, min } => {
                        let mut width = min + 1;
                        let mut i = x + width - 1;

                        loop {
                            let mut is_blank = true;
                            for j in y..=y + height - 1 {
                                if j >= source.height() {
                                    break 'map_chars;
                                }
                                if *source.get_pixel(i, j) != Rgba::from([0, 0, 0, 0]) {
                                    is_blank = false;
                                }
                            }

                            if is_blank {
                                break width - 1;
                            }

                            if width < max {
                                width += 1;
                                i = x + width - 1;
                            } else {
                                break width;
                            }
                        }
                    },
                    GlyphWidth::Monospace(width) => width,
                };

                let src_rect = URect::new(
                    x, y, x + width - 1, y + height - 1,
                );

                char_map.insert(c, PxGlyph {
                    src_rect,
                });

                x += match data.glyph_width {
                    GlyphWidth::Varied { max, min: _ } => max,
                    GlyphWidth::Monospace(width) => width,
                } + data.padding.0;

                if x >= source.width() {
                    x = 0;
                    y += height + data.padding.1;
                    if y >= source.height() {
                        break;
                    }
                }
            }

            Ok(PxFont {
                name: data.name,
                source,
                char_map,
                ascender: data.ascender,
                descender: data.descender,
                spacing: data.spacing,
            })
        })
    }

    fn extensions(&self) -> &[&str] {
        &["ron"]
    }
}