-
John Mitchell authored95a31d1a
loader.rs 11.85 KiB
use bevy::{
asset::{AssetLoader, LoadContext, LoadedAsset},
render::render_resource::{Extent3d, TextureFormat},
utils::{BoxedFuture, HashMap},
};
#[cfg(not(target_family = "wasm"))]
use bevy::asset::FileAssetIo;
use image::{EncodableLayout, RgbaImage};
use nanoserde::DeJson;
use crate::{
msdf::{self, bitmap::FloatRGBBmp, shape::Shape, ttf_parser::ShapeBuilder, vector::Vector2},
Glyph, ImageType, KayakFont, Rect, Sdf,
};
pub struct TTFLoader;
#[derive(DeJson, Default, Debug, Clone)]
pub struct KTTF {
file: String,
char_range_start: String,
char_range_end: String,
offset_x: Option<f32>,
offset_y: Option<f32>,
}
impl AssetLoader for TTFLoader {
fn load<'a>(
&'a self,
bytes: &'a [u8],
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
Box::pin(async move {
#[cfg(not(target_family = "wasm"))]
let asset_io = load_context
.asset_io()
.downcast_ref::<FileAssetIo>()
.unwrap();
let kttf: KTTF =
nanoserde::DeJson::deserialize_json(std::str::from_utf8(bytes).unwrap()).unwrap();
let char_range_start =
u32::from_str_radix(kttf.char_range_start.trim_start_matches("0x"), 16)?;
let char_range_end =
u32::from_str_radix(kttf.char_range_end.trim_start_matches("0x"), 16)?;
let font_bytes = load_context.read_asset_bytes(kttf.file.clone()).await?;
let mut cache_path = std::path::PathBuf::from(load_context.path());
let file_name = load_context
.path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string();
cache_path.set_file_name(format!("{}-cached.png", file_name));
let cache_image = load_context.read_asset_bytes(&cache_path).await;
let font_range = char_range_start..char_range_end;
let char_count = font_range.len() as u32;
let size_x = 64usize;
let size_y = 128usize;
let face = ttf_parser::Face::parse(&font_bytes, 0).unwrap();
let image_height = size_y as u32 * char_count;
let mut image_builder: RgbaImage = image::ImageBuffer::new(size_x as u32, image_height);
let mut yy = 0u32;
let mut glyphs = vec![];
// Build char to glyph mapping..
let mut glyph_to_char: HashMap<ttf_parser::GlyphId, char> =
HashMap::with_capacity(face.number_of_glyphs() as usize);
let mut char_to_glyph: HashMap<char, ttf_parser::GlyphId> =
HashMap::with_capacity(face.number_of_glyphs() as usize);
if let Some(subtable) = face.tables().cmap {
for subtable in subtable.subtables {
subtable.codepoints(|codepoint| {
if let Some(mapping) = subtable.glyph_index(codepoint) {
glyph_to_char
.insert(mapping, unsafe { std::mem::transmute(codepoint) });
char_to_glyph
.insert(unsafe { std::mem::transmute(codepoint) }, mapping);
}
})
}
}
let loaded_file = &kttf;
for char_u in font_range {
let c = char::from_u32(char_u).unwrap();
let glyph_id = *char_to_glyph.get(&c).unwrap();
let mut output = FloatRGBBmp::new(size_x, size_y);
let mut builder = ShapeBuilder::default();
let pixel_scale = size_x as f64 / face.units_per_em() as f64;
builder.pixel_scale = pixel_scale;
let _result = face.outline_glyph(glyph_id, &mut builder);
let char_bounds = face
.glyph_bounding_box(glyph_id)
.unwrap_or(ttf_parser::Rect {
x_min: 0,
x_max: size_x as i16,
y_min: 0,
y_max: size_y as i16,
});
let mut shape = builder.build();
shape.inverse_y_axis = true;
// let (left, bottom, right, top) = shape.get_bounds();
let scale = Vector2::new(1.0, 1.0);
let px_range = 2.5;
let range = px_range / scale.x.min(scale.y);
let (translation, plane) = calculate_plane(
loaded_file,
&mut shape,
pixel_scale as f32,
1.0,
px_range as f32,
1.0,
);
let advance = face.glyph_hor_advance(glyph_id).unwrap_or(0) as f32 / size_x as f32;
let c = *glyph_to_char.get(&glyph_id).unwrap();
glyphs.push(Glyph {
unicode: c,
advance: advance * pixel_scale as f32,
atlas_bounds: Some(Rect {
left: 0.0,
bottom: 0.0 as f32,
right: size_x as f32,
top: size_y as f32,
}),
plane_bounds: Some(plane),
});
// let frame = Vector2::new(size_x as f64, size_y as f64);
// dbg!((left, right, top, bottom));
// left = (left - (size_x as f64 / 8.0)).max(0.0);
// right = (right + (size_x as f64 / 8.0)).min(size_x as f64);
// top = (top + (size_y as f64 / 8.0)).min(size_y as f64);
// bottom = (bottom - (size_y as f64 / 8.0)).max(0.0);
// dbg!((left, right, top, bottom));
// let dims = Vector2::new(right - left, top - bottom);
// let translate = Vector2::new(-left + (frame.x - dims.x), (frame.y - (bottom + dims.y)) - 1.0);
if cache_image.is_err() {
msdf::edge_coloring::simple(&mut shape, 3.0, 0);
msdf::gen::generate_msdf(
&mut output,
&shape,
range,
scale,
translation + Vector2::new(0.0, size_x as f64 * 1.25),
1.11111111111111111,
);
// let left = (translation.x - char_bounds.x_min as f64 * pixel_scale).max(0.0).floor() as u32;
let right =
(translation.x + char_bounds.x_max as f64 * pixel_scale).floor() as u32;
// let top = (translation.y - char_bounds.y_min as f64 * pixel_scale).max(0.0).floor() as u32;
let bottom =
(translation.y + char_bounds.y_max as f64 * pixel_scale).floor() as u32;
for x in 0..(right + 2).min(64) {
for y in 0..bottom + 48 {
// for x in 0..size_x as u32 {
// for y in 0..size_y as u32 {
let pixel = output.get_pixel(x as usize, y as usize);
image_builder.put_pixel(
x,
yy + y,
image::Rgba([
(pixel.r * 255.0) as u8,
(pixel.g * 255.0) as u8,
(pixel.b * 255.0) as u8,
255,
]),
);
}
}
}
// if c == '\"' {
// image_builder.save("test.png").unwrap();
// panic!("");
// }
yy += size_y as u32;
}
let image_bytes = if cache_image.is_err() {
#[cfg(not(target_family = "wasm"))]
image_builder
.save(asset_io.root_path().join(cache_path))
.unwrap();
image_builder.as_bytes().to_vec()
} else {
let cache_image = cache_image.unwrap();
let image = image::load_from_memory(&cache_image).unwrap();
image.as_bytes().to_vec()
};
let mut sdf = Sdf::default();
sdf.glyphs = glyphs;
sdf.atlas.font_size = size_x as f32;
let mut image = bevy::prelude::Image::new(
Extent3d {
width: size_x as u32,
height: image_height,
depth_or_array_layers: 1,
},
bevy::render::render_resource::TextureDimension::D2,
image_bytes,
TextureFormat::Rgba8Unorm,
);
image.reinterpret_stacked_2d_as_array(char_count);
let image_handle =
load_context.set_labeled_asset("font_image", LoadedAsset::new(image));
let font = KayakFont::new(sdf, ImageType::Array(image_handle));
load_context.set_default_asset(LoadedAsset::new(font));
Ok(())
})
}
fn extensions(&self) -> &[&str] {
&["kttf"]
}
}
fn calculate_plane(
loaded_file: &KTTF,
shape: &mut Shape,
geometry_scale: f32,
scale: f32,
range: f32,
miter_limit: f32,
) -> (Vector2, Rect) {
let bounds = shape.get_bounds();
let bounds = Rect {
left: bounds.0 as f32,
bottom: bounds.1 as f32,
right: bounds.2 as f32,
top: bounds.3 as f32,
};
let scale = scale * geometry_scale;
let range = range / geometry_scale;
let (_w, _h, translation_x, translation_y) =
if bounds.left < bounds.right && bounds.bottom < bounds.top {
let mut l = bounds.left as f64;
let mut b = bounds.bottom as f64;
let mut r = bounds.right as f64;
let mut t = bounds.top as f64;
l -= 0.5 * range as f64;
b -= 0.5 * range as f64;
r += 0.5 * range as f64;
t += 0.5 * range as f64;
if miter_limit > 0.0 {
shape.bound_miters(
&mut l,
&mut b,
&mut r,
&mut t,
0.5 * range as f64,
miter_limit as f64,
1,
);
}
let w = scale as f64 * (r - l);
let h = scale as f64 * (t - b);
let box_w = w.ceil() as i32 + 1;
let box_h = h.ceil() as i32 + 1;
(
box_w,
box_h,
-l + 0.5 * (box_w as f64 - w) / scale as f64,
-b + 0.5 * (box_h as f64 - h) / scale as f64,
)
} else {
(0, 0, 0.0, 0.0)
};
// let mut l = 0.0;
// let mut r = 0.0;
// let mut b = 0.0;
// let mut t = 0.0;
// if w > 0 && h > 0 {
// let inv_box_scale = 1.0 / scale as f64;
// l = geometry_scale as f64 * (-translation_x + 0.5 * inv_box_scale);
// b = geometry_scale as f64 * (-translation_y + 0.5 * inv_box_scale);
// r = geometry_scale as f64 * (-translation_x + (w as f64 - 0.5) * inv_box_scale);
// t = geometry_scale as f64 * (-translation_y + (h as f64 - 0.5) * inv_box_scale);
// }
let left = loaded_file.offset_x.unwrap_or_default();
let top = loaded_file.offset_y.unwrap_or_default();
(
Vector2::new(translation_x, translation_y) * geometry_scale as f64,
Rect {
left: left * geometry_scale, // l as f32,
bottom: 0.0, // b as f32,
right: 0.0, // r as f32,
top: top * geometry_scale, //0.0, // t as f32,
},
)
}