core: Use correct embedded font fallback order + tests

This commit is contained in:
Nathan Adams 2023-10-19 16:41:52 +02:00
parent 8b5b135a2d
commit 64385efe48
23 changed files with 276 additions and 18 deletions

View File

@ -30,7 +30,7 @@ pub fn get_font_name<'gc>(
{ {
return Ok(AvmString::new_utf8( return Ok(AvmString::new_utf8(
activation.context.gc_context, activation.context.gc_context,
font.descriptor().class(), font.descriptor().name(),
) )
.into()); .into());
} }

View File

@ -7,6 +7,7 @@ use ruffle_render::backend::{RenderBackend, ShapeHandle};
use ruffle_render::transform::Transform; use ruffle_render::transform::Transform;
use std::cell::RefCell; use std::cell::RefCell;
use std::cmp::max; use std::cmp::max;
use std::hash::{Hash, Hasher};
pub use swf::TextGridFit; pub use swf::TextGridFit;
@ -556,21 +557,47 @@ impl Glyph {
} }
/// Structure which identifies a particular font by name and properties. /// Structure which identifies a particular font by name and properties.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Collect)] #[derive(Debug, Clone, Ord, PartialOrd, Collect)]
#[collect(require_static)] #[collect(require_static)]
pub struct FontDescriptor { pub struct FontDescriptor {
/// The name of the font.
/// This is set by the author of the SWF and does not correlate to any opentype names.
name: String, name: String,
// All name comparisons ignore case, so this is for easy comparisons.
lowercase_name: String,
is_bold: bool, is_bold: bool,
is_italic: bool, is_italic: bool,
} }
impl PartialEq for FontDescriptor {
fn eq(&self, other: &Self) -> bool {
self.lowercase_name == other.lowercase_name
&& self.is_italic == other.is_italic
&& self.is_bold == other.is_bold
}
}
impl Eq for FontDescriptor {}
impl Hash for FontDescriptor {
fn hash<H: Hasher>(&self, state: &mut H) {
self.lowercase_name.hash(state);
self.is_bold.hash(state);
self.is_italic.hash(state);
}
}
impl FontDescriptor { impl FontDescriptor {
/// Obtain a font descriptor from a SWF font tag. /// Obtain a font descriptor from a SWF font tag.
pub fn from_swf_tag(val: &swf::Font, encoding: &'static swf::Encoding) -> Self { pub fn from_swf_tag(val: &swf::Font, encoding: &'static swf::Encoding) -> Self {
let name = val.name.to_string_lossy(encoding); let name = val.name.to_string_lossy(encoding);
let lowercase_name = name.to_lowercase();
Self { Self {
name, name,
lowercase_name,
is_bold: val.flags.contains(swf::FontFlag::IS_BOLD), is_bold: val.flags.contains(swf::FontFlag::IS_BOLD),
is_italic: val.flags.contains(swf::FontFlag::IS_ITALIC), is_italic: val.flags.contains(swf::FontFlag::IS_ITALIC),
} }
@ -583,16 +610,18 @@ impl FontDescriptor {
if let Some(first_null) = name.find('\0') { if let Some(first_null) = name.find('\0') {
name.truncate(first_null); name.truncate(first_null);
}; };
let lowercase_name = name.to_lowercase();
Self { Self {
name, name,
lowercase_name,
is_bold, is_bold,
is_italic, is_italic,
} }
} }
/// Get the name of the font class this descriptor references. /// Get the name of the font this descriptor identifies.
pub fn class(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }

View File

@ -470,11 +470,15 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> {
// In an ideal world, device fonts would search for a matching font on the system and render it in some way. // In an ideal world, device fonts would search for a matching font on the system and render it in some way.
if !is_device_font { if !is_device_font {
if let Some(font) = library if let Some(font) = library
.get_font_by_name(&font_name, span.bold, span.italic) .get_embedded_font_by_name(&font_name, span.bold, span.italic)
.filter(|f| f.has_glyphs()) .filter(|f| f.has_glyphs())
{ {
return Some(font); return Some(font);
} }
// TODO: If set to use embedded fonts and we couldn't find any matching font, show nothing
// However - at time of writing, we don't support DefineFont4. If we matched this behaviour,
// then a bunch of SWFs would just show no text suddenly.
// return None;
} }
if let Some(font) = context.library.get_or_load_device_font( if let Some(font) = context.library.get_or_load_device_font(

View File

@ -146,7 +146,7 @@ impl TextFormat {
let font_class = et let font_class = et
.font_class() .font_class()
.map(|s| s.decode(encoding).into_owned()) .map(|s| s.decode(encoding).into_owned())
.or_else(|| font.map(|font| WString::from_utf8(font.descriptor().class()))) .or_else(|| font.map(|font| WString::from_utf8(font.descriptor().name())))
.unwrap_or_else(|| WString::from_utf8("Times New Roman")); .unwrap_or_else(|| WString::from_utf8("Times New Roman"));
let align = et.layout().map(|l| l.align); let align = et.layout().map(|l| l.align);
let left_margin = et.layout().map(|l| l.left_margin.to_pixels()); let left_margin = et.layout().map(|l| l.left_margin.to_pixels());

View File

@ -259,24 +259,88 @@ impl<'gc> MovieLibrary<'gc> {
} }
/// Find a font by it's name and parameters. /// Find a font by it's name and parameters.
pub fn get_font_by_name( pub fn get_embedded_font_by_name(
&self, &self,
name: &str, name: &str,
is_bold: bool, is_bold: bool,
is_italic: bool, is_italic: bool,
) -> Option<Font<'gc>> { ) -> Option<Font<'gc>> {
let descriptor = FontDescriptor::from_parts(name, is_bold, is_italic); // The order here is specific, and tested in `tests/swfs/fonts/embed_matching/fallback_preferences`
if let Some(font) = self.fonts.get(&descriptor) {
// Exact match
if let Some(font) = self
.fonts
.get(&FontDescriptor::from_parts(name, is_bold, is_italic))
{
return Some(*font); return Some(*font);
} }
// If we don't have a direct match, fallback to something with the same name
// [NA]TODO: This isn't *entirely* correct. I think we're storing fonts wrong. if is_italic ^ is_bold {
// We might need to merge fonts as they're defined, and there should only be one font per name. // If one is set (but not both), then try upgrading to bold italic...
self.fonts if let Some(font) = self
.iter() .fonts
.find(|(d, _)| d.class() == name) .get(&FontDescriptor::from_parts(name, true, true))
.map(|(_, f)| f) {
.copied() return Some(*font);
}
// and then downgrading to regular
if let Some(font) = self
.fonts
.get(&FontDescriptor::from_parts(name, false, false))
{
return Some(*font);
}
// and then finally whichever one we don't have set
if let Some(font) = self
.fonts
.get(&FontDescriptor::from_parts(name, !is_bold, !is_italic))
{
return Some(*font);
}
} else {
// We don't have an exact match and we were either looking for regular or bold-italic
if is_italic && is_bold {
// Do we have regular? (unless we already looked for it)
if let Some(font) = self
.fonts
.get(&FontDescriptor::from_parts(name, false, false))
{
return Some(*font);
}
}
// Do we have bold?
if let Some(font) = self
.fonts
.get(&FontDescriptor::from_parts(name, true, false))
{
return Some(*font);
}
// Do we have italic?
if let Some(font) = self
.fonts
.get(&FontDescriptor::from_parts(name, false, true))
{
return Some(*font);
}
if !is_bold && !is_italic {
// Do we have bold italic? (unless we already looked for it)
if let Some(font) = self
.fonts
.get(&FontDescriptor::from_parts(name, true, true))
{
return Some(*font);
}
}
}
// If there's no match at all, then it should not show anything...
None
} }
/// Returns the `Graphic` with the given character ID. /// Returns the `Graphic` with the given character ID.
@ -527,7 +591,7 @@ impl<'gc> Library<'gc> {
match definition { match definition {
FontDefinition::SwfTag(tag, encoding) => { FontDefinition::SwfTag(tag, encoding) => {
let font = Font::from_swf_tag(gc_context, renderer, tag, encoding); let font = Font::from_swf_tag(gc_context, renderer, tag, encoding);
let name = font.descriptor().class().to_owned(); let name = font.descriptor().name().to_owned();
info!("Loaded new device font \"{name}\" from swf tag"); info!("Loaded new device font \"{name}\" from swf tag");
self.device_fonts.insert(name, font); self.device_fonts.insert(name, font);
} }

View File

@ -0,0 +1,63 @@
package {
import flash.display.MovieClip;
import flash.text.TextField;
import flash.text.TextFormat;
public class Test extends MovieClip {
const NUM_ROWS: int = 20;
public function Test() {
// Bonus points: all the font names here are arbitrarily capitalised to show that equality is case insensitive
// SCP doesn't have Regular in this SWF, so see what that prefers to fall back to (spoiler: bold)
addTextField("Source Code Pro Regular", "Source code Pro", false, false, 0);
addTextField("Source Code Pro Bold", "Source code Pro", true, false, 1);
addTextField("Source Code Pro Italic", "Source code Pro", false, true, 2);
addTextField("Source Code Pro Bold Italic", "Source code Pro", true, true, 3);
// NS only has regular in this SWF, so show that they all are regular
addTextField("Noto Sans Regular", "Noto SANS", false, false, 4);
addTextField("Noto Sans Bold", "Noto SANS", true, false, 5);
addTextField("Noto Sans Italic", "Noto SANS", false, true, 6);
addTextField("Noto Sans Bold Italic", "Noto SANS", true, true, 7);
// UM NF doesn't have Regular or Bold in this SWF, so see what that prefers to fall back to (spoiler: italic)
addTextField("UbuntuMono NF Regular", "ubuntumono nf", false, false, 8);
addTextField("UbuntuMono NF Bold", "ubuntumono nf", true, false, 9);
addTextField("UbuntuMono NF Italic", "ubuntumono nf", false, true, 10);
addTextField("UbuntuMono NF Bold Italic", "ubuntumono nf", true, true, 11);
// JBM NF only has bold italic in this SWF, so show they are all bold italic
addTextField("JetBrainsMono NF Regular", "JETBRAINSMONO NF", false, false, 12);
addTextField("JetBrainsMono NF Bold", "JETBRAINSMONO NF", true, false, 13);
addTextField("JetBrainsMono NF Italic", "JETBRAINSMONO NF", false, true, 14);
addTextField("JetBrainsMono NF Bold Italic", "JETBRAINSMONO NF", true, true, 15);
// SUI doesn't have Regular or Italic, so see what the others fall back to (Bold & Bold Italic)
addTextField("Segoe UI Regular", "Segoe ui", false, false, 16);
addTextField("Segoe UI Bold", "Segoe ui", true, false, 17);
addTextField("Segoe UI Italic", "Segoe ui", false, true, 18);
addTextField("Segoe UI Bold Italic", "Segoe ui", true, true, 19);
}
function addTextField(text: String, font: String, bold: Boolean, italic: Boolean, y: int) {
var textField: TextField = new TextField();
var textFormat: TextFormat = new TextFormat();
textFormat.font = font;
textFormat.italic = italic;
textFormat.bold = bold;
textFormat.size = 30;
textField.defaultTextFormat = textFormat;
textField.embedFonts = true;
textField.text = text;
textField.y = Math.floor(stage.stageHeight / NUM_ROWS) * y;
textField.width = stage.stageWidth;
addChild(textField);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,7 @@
num_frames = 1
[image_comparisons.output]
tolerance = 3
[player_options]
with_renderer = { optional = false, sample_count = 1 }

View File

@ -0,0 +1,36 @@
package {
import flash.display.MovieClip;
import flash.text.TextField;
import flash.text.TextFormat;
public class Test extends MovieClip {
const NUM_ROWS: int = 4;
public function Test() {
addTextField("Source Code Pro Regular", "Source Code Pro", false, false, 0);
addTextField("Source Code Pro Bold", "Source Code Pro", true, false, 1);
addTextField("Source Code Pro Italic", "Source Code Pro", false, true, 2);
addTextField("Source Code Pro Bold Italic", "Source Code Pro", true, true, 3);
}
function addTextField(text: String, font: String, bold: Boolean, italic: Boolean, y: int) {
var textField: TextField = new TextField();
var textFormat: TextFormat = new TextFormat();
textFormat.font = font;
textFormat.italic = italic;
textFormat.bold = bold;
textFormat.size = 30;
textField.defaultTextFormat = textFormat;
textField.embedFonts = true;
textField.text = text;
textField.y = Math.floor(stage.stageHeight / NUM_ROWS) * y;
textField.width = stage.stageWidth;
addChild(textField);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -0,0 +1,9 @@
# This test shows that it ignores missing glyphs when matching embedded fonts, choosing only on style and name
num_frames = 1
[image_comparisons.output]
tolerance = 3
[player_options]
with_renderer = { optional = false, sample_count = 1 }

View File

@ -0,0 +1,36 @@
package {
import flash.display.MovieClip;
import flash.text.TextField;
import flash.text.TextFormat;
public class Test extends MovieClip {
const NUM_ROWS: int = 4;
public function Test() {
addTextField("Source Code Pro Regular", "Source Code Pro", false, false, 0);
addTextField("Source Code Pro Bold", "Source Code Pro", true, false, 1);
addTextField("Source Code Pro Italic", "Source Code Pro", false, true, 2);
addTextField("Source Code Pro Bold Italic", "Source Code Pro", true, true, 3);
}
function addTextField(text: String, font: String, bold: Boolean, italic: Boolean, y: int) {
var textField: TextField = new TextField();
var textFormat: TextFormat = new TextFormat();
textFormat.font = font;
textFormat.italic = italic;
textFormat.bold = bold;
textFormat.size = 30;
textField.defaultTextFormat = textFormat;
textField.embedFonts = true;
textField.text = text;
textField.y = Math.floor(stage.stageHeight / NUM_ROWS) * y;
textField.width = stage.stageWidth;
addChild(textField);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,10 @@
# There are no fonts embedded in this swf. It should not render anything at all, or error.
num_frames = 1
known_failure = true # Right now we intentionally fall back, because we don't support DefineFont4 embedded fonts yet
[image_comparisons.output]
tolerance = 0
[player_options]
with_renderer = { optional = false, sample_count = 1 }