diff --git a/core/src/avm2/globals/flash/text/font.rs b/core/src/avm2/globals/flash/text/font.rs index 6426b798c..4cfbbb472 100644 --- a/core/src/avm2/globals/flash/text/font.rs +++ b/core/src/avm2/globals/flash/text/font.rs @@ -30,7 +30,7 @@ pub fn get_font_name<'gc>( { return Ok(AvmString::new_utf8( activation.context.gc_context, - font.descriptor().class(), + font.descriptor().name(), ) .into()); } diff --git a/core/src/font.rs b/core/src/font.rs index c05ec4a38..9fa6d565b 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -7,6 +7,7 @@ use ruffle_render::backend::{RenderBackend, ShapeHandle}; use ruffle_render::transform::Transform; use std::cell::RefCell; use std::cmp::max; +use std::hash::{Hash, Hasher}; pub use swf::TextGridFit; @@ -556,21 +557,47 @@ impl Glyph { } /// 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)] 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, + + // All name comparisons ignore case, so this is for easy comparisons. + lowercase_name: String, + is_bold: 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(&self, state: &mut H) { + self.lowercase_name.hash(state); + self.is_bold.hash(state); + self.is_italic.hash(state); + } +} + impl FontDescriptor { /// Obtain a font descriptor from a SWF font tag. pub fn from_swf_tag(val: &swf::Font, encoding: &'static swf::Encoding) -> Self { let name = val.name.to_string_lossy(encoding); + let lowercase_name = name.to_lowercase(); Self { name, + lowercase_name, is_bold: val.flags.contains(swf::FontFlag::IS_BOLD), is_italic: val.flags.contains(swf::FontFlag::IS_ITALIC), } @@ -583,16 +610,18 @@ impl FontDescriptor { if let Some(first_null) = name.find('\0') { name.truncate(first_null); }; + let lowercase_name = name.to_lowercase(); Self { name, + lowercase_name, is_bold, is_italic, } } - /// Get the name of the font class this descriptor references. - pub fn class(&self) -> &str { + /// Get the name of the font this descriptor identifies. + pub fn name(&self) -> &str { &self.name } diff --git a/core/src/html/layout.rs b/core/src/html/layout.rs index ab542c72f..c8545bf17 100644 --- a/core/src/html/layout.rs +++ b/core/src/html/layout.rs @@ -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. if !is_device_font { 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()) { 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( diff --git a/core/src/html/text_format.rs b/core/src/html/text_format.rs index 055a1d492..527a65e6a 100644 --- a/core/src/html/text_format.rs +++ b/core/src/html/text_format.rs @@ -146,7 +146,7 @@ impl TextFormat { let font_class = et .font_class() .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")); let align = et.layout().map(|l| l.align); let left_margin = et.layout().map(|l| l.left_margin.to_pixels()); diff --git a/core/src/library.rs b/core/src/library.rs index 48cefdaf9..c44e00af3 100644 --- a/core/src/library.rs +++ b/core/src/library.rs @@ -259,24 +259,88 @@ impl<'gc> MovieLibrary<'gc> { } /// Find a font by it's name and parameters. - pub fn get_font_by_name( + pub fn get_embedded_font_by_name( &self, name: &str, is_bold: bool, is_italic: bool, ) -> Option> { - let descriptor = FontDescriptor::from_parts(name, is_bold, is_italic); - if let Some(font) = self.fonts.get(&descriptor) { + // The order here is specific, and tested in `tests/swfs/fonts/embed_matching/fallback_preferences` + + // Exact match + if let Some(font) = self + .fonts + .get(&FontDescriptor::from_parts(name, is_bold, is_italic)) + { 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. - // We might need to merge fonts as they're defined, and there should only be one font per name. - self.fonts - .iter() - .find(|(d, _)| d.class() == name) - .map(|(_, f)| f) - .copied() + + if is_italic ^ is_bold { + // If one is set (but not both), then try upgrading to bold italic... + if let Some(font) = self + .fonts + .get(&FontDescriptor::from_parts(name, true, true)) + { + 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. @@ -527,7 +591,7 @@ impl<'gc> Library<'gc> { match definition { FontDefinition::SwfTag(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"); self.device_fonts.insert(name, font); } diff --git a/tests/tests/swfs/fonts/embed_matching/fallback_preferences/Test.as b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/Test.as new file mode 100644 index 000000000..7cfa7309b --- /dev/null +++ b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/Test.as @@ -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); + } + } + +} diff --git a/tests/tests/swfs/fonts/embed_matching/fallback_preferences/output.expected.png b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/output.expected.png new file mode 100644 index 000000000..8fedc6a9c Binary files /dev/null and b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/output.expected.png differ diff --git a/tests/tests/swfs/fonts/embed_matching/fallback_preferences/output.txt b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/output.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tests/swfs/fonts/embed_matching/fallback_preferences/test.fla b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/test.fla new file mode 100644 index 000000000..934667f29 Binary files /dev/null and b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/test.fla differ diff --git a/tests/tests/swfs/fonts/embed_matching/fallback_preferences/test.swf b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/test.swf new file mode 100644 index 000000000..9510b5f16 Binary files /dev/null and b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/test.swf differ diff --git a/tests/tests/swfs/fonts/embed_matching/fallback_preferences/test.toml b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/test.toml new file mode 100644 index 000000000..37b7f9341 --- /dev/null +++ b/tests/tests/swfs/fonts/embed_matching/fallback_preferences/test.toml @@ -0,0 +1,7 @@ +num_frames = 1 + +[image_comparisons.output] +tolerance = 3 + +[player_options] +with_renderer = { optional = false, sample_count = 1 } diff --git a/tests/tests/swfs/fonts/embed_matching/match_style/Test.as b/tests/tests/swfs/fonts/embed_matching/match_style/Test.as new file mode 100644 index 000000000..7ee30bd1b --- /dev/null +++ b/tests/tests/swfs/fonts/embed_matching/match_style/Test.as @@ -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); + } + } + +} diff --git a/tests/tests/swfs/fonts/embed_matching/match_style/output.expected.png b/tests/tests/swfs/fonts/embed_matching/match_style/output.expected.png new file mode 100644 index 000000000..f49774a7e Binary files /dev/null and b/tests/tests/swfs/fonts/embed_matching/match_style/output.expected.png differ diff --git a/tests/tests/swfs/fonts/embed_matching/match_style/output.txt b/tests/tests/swfs/fonts/embed_matching/match_style/output.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tests/swfs/fonts/embed_matching/match_style/test.fla b/tests/tests/swfs/fonts/embed_matching/match_style/test.fla new file mode 100644 index 000000000..6920145f8 Binary files /dev/null and b/tests/tests/swfs/fonts/embed_matching/match_style/test.fla differ diff --git a/tests/tests/swfs/fonts/embed_matching/match_style/test.swf b/tests/tests/swfs/fonts/embed_matching/match_style/test.swf new file mode 100644 index 000000000..b4bb846d3 Binary files /dev/null and b/tests/tests/swfs/fonts/embed_matching/match_style/test.swf differ diff --git a/tests/tests/swfs/fonts/embed_matching/match_style/test.toml b/tests/tests/swfs/fonts/embed_matching/match_style/test.toml new file mode 100644 index 000000000..768152fa7 --- /dev/null +++ b/tests/tests/swfs/fonts/embed_matching/match_style/test.toml @@ -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 } diff --git a/tests/tests/swfs/fonts/embed_matching/no_font_found/Test.as b/tests/tests/swfs/fonts/embed_matching/no_font_found/Test.as new file mode 100644 index 000000000..7ee30bd1b --- /dev/null +++ b/tests/tests/swfs/fonts/embed_matching/no_font_found/Test.as @@ -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); + } + } + +} diff --git a/tests/tests/swfs/fonts/embed_matching/no_font_found/output.expected.png b/tests/tests/swfs/fonts/embed_matching/no_font_found/output.expected.png new file mode 100644 index 000000000..4e47eaa08 Binary files /dev/null and b/tests/tests/swfs/fonts/embed_matching/no_font_found/output.expected.png differ diff --git a/tests/tests/swfs/fonts/embed_matching/no_font_found/output.txt b/tests/tests/swfs/fonts/embed_matching/no_font_found/output.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tests/swfs/fonts/embed_matching/no_font_found/test.fla b/tests/tests/swfs/fonts/embed_matching/no_font_found/test.fla new file mode 100644 index 000000000..65200f746 Binary files /dev/null and b/tests/tests/swfs/fonts/embed_matching/no_font_found/test.fla differ diff --git a/tests/tests/swfs/fonts/embed_matching/no_font_found/test.swf b/tests/tests/swfs/fonts/embed_matching/no_font_found/test.swf new file mode 100644 index 000000000..f8bde99c3 Binary files /dev/null and b/tests/tests/swfs/fonts/embed_matching/no_font_found/test.swf differ diff --git a/tests/tests/swfs/fonts/embed_matching/no_font_found/test.toml b/tests/tests/swfs/fonts/embed_matching/no_font_found/test.toml new file mode 100644 index 000000000..79c486db1 --- /dev/null +++ b/tests/tests/swfs/fonts/embed_matching/no_font_found/test.toml @@ -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 }