core: Add FontFace struct, for loading a Font from a file binary

This commit is contained in:
Nathan Adams 2023-09-15 18:59:29 +02:00
parent d97314d315
commit 7e4ac986f2
6 changed files with 229 additions and 2 deletions

1
Cargo.lock generated
View File

@ -4289,6 +4289,7 @@ dependencies = [
"symphonia",
"thiserror",
"tracing",
"ttf-parser",
"url",
"wasm-bindgen-futures",
"weak-table",

View File

@ -62,6 +62,7 @@ async-channel = "2.1.1"
jpegxr = { git = "https://github.com/ruffle-rs/jpegxr", branch = "ruffle", optional = true }
image = { version = "0.24.7", default-features = false, features = ["tiff", "dxt"] }
enum-map = "2.7.3"
ttf-parser = "0.20"
[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
version = "0.3.30"

View File

@ -15,6 +15,14 @@ pub static US_ENGLISH: LanguageIdentifier = langid!("en-US");
pub enum FontDefinition<'a> {
/// A singular DefineFont tag extracted from a swf.
SwfTag(swf::Font<'a>, &'static swf::Encoding),
/// A font contained in an external file, such as a ttf.
FontFile {
name: String,
is_bold: bool,
is_italic: bool,
data: Vec<u8>,
},
}
/// A filter specifying a category that can be selected from a file chooser dialog

View File

@ -404,7 +404,7 @@ impl Drawing {
}
// Ensures that the path is closed for a pending fill.
fn close_path(&mut self) {
pub fn close_path(&mut self) {
if let Some(fill) = &mut self.current_fill {
if self.cursor != self.fill_start {
fill.commands.push(DrawCommand::LineTo(self.fill_start));

View File

@ -1,13 +1,17 @@
use crate::drawing::Drawing;
use crate::html::TextSpan;
use crate::prelude::*;
use crate::string::WStr;
use gc_arena::{Collect, Gc, Mutation};
use ruffle_render::backend::null::NullBitmapSource;
use ruffle_render::backend::{RenderBackend, ShapeHandle};
use ruffle_render::shape_utils::{DrawCommand, FillRule};
use ruffle_render::transform::Transform;
use std::cell::RefCell;
use std::borrow::Cow;
use std::cell::{OnceCell, RefCell};
use std::cmp::max;
use std::hash::{Hash, Hasher};
use swf::FillStyle;
pub use swf::TextGridFit;
@ -87,6 +91,169 @@ impl EvalParameters {
}
}
struct GlyphToDrawing<'a>(&'a mut Drawing);
/// Convert from a TTF outline, to a flash Drawing.
///
/// Note that the Y axis is flipped. I do not know why, but Flash does this.
impl<'a> ttf_parser::OutlineBuilder for GlyphToDrawing<'a> {
fn move_to(&mut self, x: f32, y: f32) {
self.0.draw_command(DrawCommand::MoveTo(Point::new(
Twips::new(x as i32),
Twips::new(-y as i32),
)));
}
fn line_to(&mut self, x: f32, y: f32) {
self.0.draw_command(DrawCommand::LineTo(Point::new(
Twips::new(x as i32),
Twips::new(-y as i32),
)));
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.0.draw_command(DrawCommand::QuadraticCurveTo {
control: Point::new(Twips::new(x1 as i32), Twips::new(-y1 as i32)),
anchor: Point::new(Twips::new(x as i32), Twips::new(-y as i32)),
});
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.0.draw_command(DrawCommand::CubicCurveTo {
control_a: Point::new(Twips::new(x1 as i32), Twips::new(-y1 as i32)),
control_b: Point::new(Twips::new(x2 as i32), Twips::new(-y2 as i32)),
anchor: Point::new(Twips::new(x as i32), Twips::new(-y as i32)),
});
}
fn close(&mut self) {
self.0.close_path();
}
}
/// Represents a raw font file (ie .ttf).
/// This should be shared and reused where possible, and it's reparsed every time a new glyph is required.
///
/// Parsing of a font is near-free (according to [ttf_parser::Face::parse]), but the storage isn't.
///
/// Font files may contain multiple individual font faces, but those font faces may reuse the same
/// Glyph from the same file. For this reason, glyphs are reused where possible.
#[derive(Debug)]
pub struct FontFace {
bytes: Cow<'static, [u8]>,
glyphs: Vec<OnceCell<Option<Glyph>>>,
font_index: u32,
ascender: i32,
descender: i32,
leading: i16,
scale: f32,
might_have_kerning: bool,
}
impl FontFace {
pub fn new(
bytes: Cow<'static, [u8]>,
font_index: u32,
) -> Result<Self, ttf_parser::FaceParsingError> {
// TODO: Support font collections
// We validate that the font is good here, so we can just `.expect()` it later
let face = ttf_parser::Face::parse(&bytes, font_index)?;
let ascender = face.ascender() as i32;
let descender = -face.descender() as i32;
let leading = face.line_gap();
let scale = face.units_per_em() as f32;
let glyphs = vec![OnceCell::new(); face.number_of_glyphs() as usize];
// [NA] TODO: This is technically correct for just Kerning, but in practice kerning comes in many forms.
// We need to support GPOS to do better at this, but that's a bigger change to font rendering as a whole.
let might_have_kerning = face
.tables()
.kern
.map(|k| {
k.subtables
.into_iter()
.any(|sub| sub.horizontal && !sub.has_state_machine)
})
.unwrap_or_default();
Ok(Self {
bytes,
font_index,
glyphs,
ascender,
descender,
leading,
scale,
might_have_kerning,
})
}
pub fn get_glyph(&self, character: char) -> Option<&Glyph> {
let face = ttf_parser::Face::parse(&self.bytes, self.font_index)
.expect("Font was already checked to be valid");
if let Some(glyph_id) = face.glyph_index(character) {
return self.glyphs[glyph_id.0 as usize]
.get_or_init(|| {
let mut drawing = Drawing::new();
drawing.set_winding_rule(FillRule::NonZero); // TTF uses NonZero
drawing.set_fill_style(Some(FillStyle::Color(Color::WHITE)));
if face
.outline_glyph(glyph_id, &mut GlyphToDrawing(&mut drawing))
.is_some()
{
let advance = face.glyph_hor_advance(glyph_id).map_or_else(
|| drawing.self_bounds().width(),
|a| Twips::new(a as i32),
);
Some(Glyph {
shape_handle: Default::default(),
shape: GlyphShape::Drawing(drawing),
advance,
})
} else {
let advance = Twips::new(face.glyph_hor_advance(glyph_id)? as i32);
// If we have advance, then this is either an image, SVG or simply missing (ie whitespace)
Some(Glyph {
shape_handle: Default::default(),
shape: GlyphShape::None,
advance,
})
}
})
.as_ref();
}
None
}
pub fn has_kerning_info(&self) -> bool {
self.might_have_kerning
}
pub fn get_kerning_offset(&self, left: char, right: char) -> Twips {
let face = ttf_parser::Face::parse(&self.bytes, self.font_index)
.expect("Font was already checked to be valid");
if let (Some(left_glyph), Some(right_glyph)) =
(face.glyph_index(left), face.glyph_index(right))
{
if let Some(kern) = face.tables().kern {
for subtable in kern.subtables {
if subtable.horizontal {
if let Some(value) = subtable.glyphs_kerning(left_glyph, right_glyph) {
return Twips::from_pixels_i32(value as i32);
}
}
}
}
}
Twips::ZERO
}
}
#[derive(Debug)]
pub enum GlyphSource {
Memory {
@ -102,6 +269,7 @@ pub enum GlyphSource {
/// Maps from a pair of unicode code points to horizontal offset value.
kerning_pairs: fnv::FnvHashMap<(u16, u16), Twips>,
},
FontFace(FontFace),
Empty,
}
@ -109,6 +277,7 @@ impl GlyphSource {
pub fn get_by_index(&self, index: usize) -> Option<&Glyph> {
match self {
GlyphSource::Memory { glyphs, .. } => glyphs.get(index),
GlyphSource::FontFace(_) => None, // Unsupported.
GlyphSource::Empty => None,
}
}
@ -128,6 +297,7 @@ impl GlyphSource {
None
}
}
GlyphSource::FontFace(face) => face.get_glyph(code_point),
GlyphSource::Empty => None,
}
}
@ -135,6 +305,7 @@ impl GlyphSource {
pub fn has_kerning_info(&self) -> bool {
match self {
GlyphSource::Memory { kerning_pairs, .. } => !kerning_pairs.is_empty(),
GlyphSource::FontFace(face) => face.has_kerning_info(),
GlyphSource::Empty => false,
}
}
@ -150,6 +321,7 @@ impl GlyphSource {
.cloned()
.unwrap_or_default()
}
GlyphSource::FontFace(face) => face.get_kerning_offset(left, right),
GlyphSource::Empty => Twips::ZERO,
}
}
@ -195,6 +367,28 @@ struct FontData {
}
impl<'gc> Font<'gc> {
pub fn from_font_file(
gc_context: &Mutation<'gc>,
descriptor: FontDescriptor,
bytes: Cow<'static, [u8]>,
font_index: u32,
) -> Result<Font<'gc>, ttf_parser::FaceParsingError> {
let face = FontFace::new(bytes, font_index)?;
Ok(Font(Gc::new(
gc_context,
FontData {
scale: face.scale,
ascent: face.ascender,
descent: face.descender,
leading: face.leading,
glyphs: GlyphSource::FontFace(face),
descriptor,
font_type: FontType::Device,
},
)))
}
pub fn from_swf_tag(
gc_context: &Mutation<'gc>,
renderer: &mut dyn RenderBackend,
@ -544,6 +738,8 @@ impl SwfGlyphOrShape {
#[derive(Debug, Clone)]
enum GlyphShape {
Swf(RefCell<SwfGlyphOrShape>),
Drawing(Drawing),
None,
}
impl GlyphShape {
@ -555,6 +751,8 @@ impl GlyphShape {
shape.shape_bounds.contains(point)
&& ruffle_render::shape_utils::shape_hit_test(shape, point, local_matrix)
}
GlyphShape::Drawing(drawing) => drawing.hit_test(point, local_matrix),
GlyphShape::None => false,
}
}
@ -564,6 +762,8 @@ impl GlyphShape {
let mut glyph = glyph.borrow_mut();
Some(renderer.register_shape((&*glyph.shape()).into(), &NullBitmapSource))
}
GlyphShape::Drawing(drawing) => Some(drawing.register_or_replace(renderer)),
GlyphShape::None => None,
}
}
}

View File

@ -2,6 +2,7 @@ use crate::avm1::{PropertyMap as Avm1PropertyMap, PropertyMap};
use crate::avm2::{ClassObject as Avm2ClassObject, Domain as Avm2Domain};
use crate::backend::audio::SoundHandle;
use crate::character::Character;
use std::borrow::Cow;
use crate::display_object::{Bitmap, Graphic, MorphShape, TDisplayObject, Text};
use crate::font::{Font, FontDescriptor, FontType};
@ -534,6 +535,22 @@ impl<'gc> Library<'gc> {
info!("Loaded new device font \"{name}\" from swf tag");
self.device_fonts.register(font);
}
FontDefinition::FontFile {
name,
is_bold,
is_italic,
data,
} => {
let descriptor = FontDescriptor::from_parts(&name, is_bold, is_italic);
if let Ok(font) = Font::from_font_file(gc_context, descriptor, Cow::Owned(data), 0)
{
let name = font.descriptor().name().to_owned();
info!("Loaded new device font \"{name}\" from file");
self.device_fonts.register(font);
} else {
warn!("Failed to load device font from file");
}
}
}
self.default_font_cache.clear();
}