core: Add FontFace struct, for loading a Font from a file binary
This commit is contained in:
parent
d97314d315
commit
7e4ac986f2
|
@ -4289,6 +4289,7 @@ dependencies = [
|
|||
"symphonia",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"ttf-parser",
|
||||
"url",
|
||||
"wasm-bindgen-futures",
|
||||
"weak-table",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
202
core/src/font.rs
202
core/src/font.rs
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue