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",
|
"symphonia",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"ttf-parser",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"weak-table",
|
"weak-table",
|
||||||
|
|
|
@ -62,6 +62,7 @@ async-channel = "2.1.1"
|
||||||
jpegxr = { git = "https://github.com/ruffle-rs/jpegxr", branch = "ruffle", optional = true }
|
jpegxr = { git = "https://github.com/ruffle-rs/jpegxr", branch = "ruffle", optional = true }
|
||||||
image = { version = "0.24.7", default-features = false, features = ["tiff", "dxt"] }
|
image = { version = "0.24.7", default-features = false, features = ["tiff", "dxt"] }
|
||||||
enum-map = "2.7.3"
|
enum-map = "2.7.3"
|
||||||
|
ttf-parser = "0.20"
|
||||||
|
|
||||||
[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
|
[target.'cfg(not(target_family = "wasm"))'.dependencies.futures]
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
|
|
|
@ -15,6 +15,14 @@ pub static US_ENGLISH: LanguageIdentifier = langid!("en-US");
|
||||||
pub enum FontDefinition<'a> {
|
pub enum FontDefinition<'a> {
|
||||||
/// A singular DefineFont tag extracted from a swf.
|
/// A singular DefineFont tag extracted from a swf.
|
||||||
SwfTag(swf::Font<'a>, &'static swf::Encoding),
|
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
|
/// 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.
|
// 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 let Some(fill) = &mut self.current_fill {
|
||||||
if self.cursor != self.fill_start {
|
if self.cursor != self.fill_start {
|
||||||
fill.commands.push(DrawCommand::LineTo(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::html::TextSpan;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::string::WStr;
|
use crate::string::WStr;
|
||||||
use gc_arena::{Collect, Gc, Mutation};
|
use gc_arena::{Collect, Gc, Mutation};
|
||||||
use ruffle_render::backend::null::NullBitmapSource;
|
use ruffle_render::backend::null::NullBitmapSource;
|
||||||
use ruffle_render::backend::{RenderBackend, ShapeHandle};
|
use ruffle_render::backend::{RenderBackend, ShapeHandle};
|
||||||
|
use ruffle_render::shape_utils::{DrawCommand, FillRule};
|
||||||
use ruffle_render::transform::Transform;
|
use ruffle_render::transform::Transform;
|
||||||
use std::cell::RefCell;
|
use std::borrow::Cow;
|
||||||
|
use std::cell::{OnceCell, RefCell};
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
use swf::FillStyle;
|
||||||
|
|
||||||
pub use swf::TextGridFit;
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum GlyphSource {
|
pub enum GlyphSource {
|
||||||
Memory {
|
Memory {
|
||||||
|
@ -102,6 +269,7 @@ pub enum GlyphSource {
|
||||||
/// Maps from a pair of unicode code points to horizontal offset value.
|
/// Maps from a pair of unicode code points to horizontal offset value.
|
||||||
kerning_pairs: fnv::FnvHashMap<(u16, u16), Twips>,
|
kerning_pairs: fnv::FnvHashMap<(u16, u16), Twips>,
|
||||||
},
|
},
|
||||||
|
FontFace(FontFace),
|
||||||
Empty,
|
Empty,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +277,7 @@ impl GlyphSource {
|
||||||
pub fn get_by_index(&self, index: usize) -> Option<&Glyph> {
|
pub fn get_by_index(&self, index: usize) -> Option<&Glyph> {
|
||||||
match self {
|
match self {
|
||||||
GlyphSource::Memory { glyphs, .. } => glyphs.get(index),
|
GlyphSource::Memory { glyphs, .. } => glyphs.get(index),
|
||||||
|
GlyphSource::FontFace(_) => None, // Unsupported.
|
||||||
GlyphSource::Empty => None,
|
GlyphSource::Empty => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,6 +297,7 @@ impl GlyphSource {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GlyphSource::FontFace(face) => face.get_glyph(code_point),
|
||||||
GlyphSource::Empty => None,
|
GlyphSource::Empty => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,6 +305,7 @@ impl GlyphSource {
|
||||||
pub fn has_kerning_info(&self) -> bool {
|
pub fn has_kerning_info(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
GlyphSource::Memory { kerning_pairs, .. } => !kerning_pairs.is_empty(),
|
GlyphSource::Memory { kerning_pairs, .. } => !kerning_pairs.is_empty(),
|
||||||
|
GlyphSource::FontFace(face) => face.has_kerning_info(),
|
||||||
GlyphSource::Empty => false,
|
GlyphSource::Empty => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,6 +321,7 @@ impl GlyphSource {
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
GlyphSource::FontFace(face) => face.get_kerning_offset(left, right),
|
||||||
GlyphSource::Empty => Twips::ZERO,
|
GlyphSource::Empty => Twips::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,6 +367,28 @@ struct FontData {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'gc> Font<'gc> {
|
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(
|
pub fn from_swf_tag(
|
||||||
gc_context: &Mutation<'gc>,
|
gc_context: &Mutation<'gc>,
|
||||||
renderer: &mut dyn RenderBackend,
|
renderer: &mut dyn RenderBackend,
|
||||||
|
@ -544,6 +738,8 @@ impl SwfGlyphOrShape {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum GlyphShape {
|
enum GlyphShape {
|
||||||
Swf(RefCell<SwfGlyphOrShape>),
|
Swf(RefCell<SwfGlyphOrShape>),
|
||||||
|
Drawing(Drawing),
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GlyphShape {
|
impl GlyphShape {
|
||||||
|
@ -555,6 +751,8 @@ impl GlyphShape {
|
||||||
shape.shape_bounds.contains(point)
|
shape.shape_bounds.contains(point)
|
||||||
&& ruffle_render::shape_utils::shape_hit_test(shape, point, local_matrix)
|
&& 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();
|
let mut glyph = glyph.borrow_mut();
|
||||||
Some(renderer.register_shape((&*glyph.shape()).into(), &NullBitmapSource))
|
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::avm2::{ClassObject as Avm2ClassObject, Domain as Avm2Domain};
|
||||||
use crate::backend::audio::SoundHandle;
|
use crate::backend::audio::SoundHandle;
|
||||||
use crate::character::Character;
|
use crate::character::Character;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::display_object::{Bitmap, Graphic, MorphShape, TDisplayObject, Text};
|
use crate::display_object::{Bitmap, Graphic, MorphShape, TDisplayObject, Text};
|
||||||
use crate::font::{Font, FontDescriptor, FontType};
|
use crate::font::{Font, FontDescriptor, FontType};
|
||||||
|
@ -534,6 +535,22 @@ impl<'gc> Library<'gc> {
|
||||||
info!("Loaded new device font \"{name}\" from swf tag");
|
info!("Loaded new device font \"{name}\" from swf tag");
|
||||||
self.device_fonts.register(font);
|
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();
|
self.default_font_cache.clear();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue