Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

text: Allow specifying multiple device fonts for text fields #19274

Merged
merged 5 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions core/src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ pub enum DefaultFont {
JapaneseMincho,
}

impl DefaultFont {
pub fn from_name(name: &str) -> Option<Self> {
Some(match name {
"_serif" => DefaultFont::Serif,
"_sans" => DefaultFont::Sans,
"_typewriter" => DefaultFont::Typewriter,
"_ゴシック" => DefaultFont::JapaneseGothic,
"_等幅" => DefaultFont::JapaneseGothicMono,
"_明朝" => DefaultFont::JapaneseMincho,
_ => return None,
})
}
}

fn round_to_pixel(t: Twips) -> Twips {
Twips::from_pixels(t.to_pixels().round())
}
Expand Down Expand Up @@ -333,6 +347,12 @@ pub enum FontType {
Device,
}

impl FontType {
pub fn is_embedded(self) -> bool {
self != Self::Device
}
}

#[derive(Debug, Clone, Collect, Copy)]
#[collect(no_drop)]
pub struct Font<'gc>(Gc<'gc, FontData>);
Expand Down
85 changes: 44 additions & 41 deletions core/src/html/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use ruffle_render::shape_utils::DrawCommand;
use std::cmp::{max, min, Ordering};
use std::fmt::{Debug, Formatter};
use std::mem;
use std::ops::{Deref, Range};
use std::ops::Range;
use std::slice::Iter;
use std::sync::Arc;
use swf::{Point, Rectangle, Twips};
Expand Down Expand Up @@ -646,7 +646,7 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> {

// Note that the SWF can still contain a DefineFont tag with no glyphs/layout info in this case (see #451).
// In an ideal world, device fonts would search for a matching font on the system and render it in some way.
if self.font_type != FontType::Device {
if self.font_type.is_embedded() {
if let Some(font) = context
.library
.get_embedded_font_by_name(
Expand All @@ -666,48 +666,51 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> {
// return new_empty_font(context, span, self.font_type);
}

// Check if the font name is one of the known default fonts.
if let Some(default_font) = match font_name.deref() {
"_serif" => Some(DefaultFont::Serif),
"_sans" => Some(DefaultFont::Sans),
"_typewriter" => Some(DefaultFont::Typewriter),
"_ゴシック" => Some(DefaultFont::JapaneseGothic),
"_等幅" => Some(DefaultFont::JapaneseGothicMono),
"_明朝" => Some(DefaultFont::JapaneseMincho),
_ => None,
} {
if let Some(&font) = context
.library
.default_font(
default_font,
span.style.bold,
span.style.italic,
context.ui,
context.renderer,
context.gc_context,
)
.first()
{
// Specifying multiple font names is supported only for device fonts.
let font_names: Vec<&str> = font_name.split(",").collect();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this work if you iterated on the split without collecting into a Vec?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could do that, sure, but then I'm using the vec to get the fallback font name and IMO it's clearer when I do font_names.first() compared to not collecting into a vec and doing something else.

for font_name in &font_names {
let font_name = font_name.trim();

// Check if the font name is one of the known default fonts.
if let Some(default_font) = DefaultFont::from_name(font_name) {
if let Some(&font) = context
.library
.default_font(
default_font,
span.style.bold,
span.style.italic,
context.ui,
context.renderer,
context.gc_context,
)
.first()
{
return font;
} else {
let font_desc = describe_font(span);
tracing::error!(
"Known default device font not found: {font_desc}, text will be missing"
);
return new_empty_font(context, span, self.font_type);
}
}

if let Some(font) = context.library.get_or_load_device_font(
font_name,
span.style.bold,
span.style.italic,
context.ui,
context.renderer,
context.gc_context,
) {
return font;
} else {
let font_desc = describe_font(span);
tracing::error!(
"Known default device font not found: {font_desc}, text will be missing"
);
return new_empty_font(context, span, self.font_type);
}
}

if let Some(font) = context.library.get_or_load_device_font(
&font_name,
span.style.bold,
span.style.italic,
context.ui,
context.renderer,
context.gc_context,
) {
return font;
}
// TODO We fall back to the default font based on the first font in the list.
// This is mainly to preserve old behavior, that might change when we
// implement a proper fallback.
let font_name = font_names.first().copied().unwrap_or("");

// TODO: handle multiple fonts for a definition, each covering different sets of glyphs

Expand All @@ -716,7 +719,7 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> {
// well-known aliases for the default fonts for better compatibility
// with devices that don't have those fonts installed. As a last resort
// we fall back to using sans (like Flash).
let default_font = match font_name.deref() {
let default_font = match font_name {
"Times New Roman" => DefaultFont::Serif,
"Arial" => DefaultFont::Sans,
"Consolas" => DefaultFont::Typewriter,
Expand Down
5 changes: 4 additions & 1 deletion tests/framework/src/backends/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ impl UiBackend for TestUiBackend {
register: &mut dyn FnMut(FontDefinition),
) {
for font in &self.fonts {
if font.family != name || font.bold != is_bold || font.italic != is_italic {
if !font.family.eq_ignore_ascii_case(name)
|| font.bold != is_bold
|| font.italic != is_italic
{
continue;
}

Expand Down
106 changes: 106 additions & 0 deletions tests/tests/swfs/fonts/device_font_list/Test.as
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package {
import flash.display.*;
import flash.text.*;

[SWF(width="100", height="200")]
public class Test extends Sprite {
[Embed(source="TestFontA.ttf", fontName="EmbeddedTestFontA", embedAsCFF="false", unicodeRange="U+0061-U+0064")]
private var EmbeddedTestFontA:Class;

[Embed(source="TestFontB.ttf", fontName="EmbeddedTestFontB", embedAsCFF="false", unicodeRange="U+0061-U+0064")]
private var EmbeddedTestFontB:Class;

private var nextY: Number = 0;

private var fontListsDevice: Array = [
"Totally Unknown, TestFontA , TestFontB",
" testFOntB , TestFontA , TestFontB",
];
private var fontListsEmbedded: Array = [
"Totally Unknown, EmbeddedTestFontA, EmbeddedTestFontB",
"EmbeddedTestFontA",
" EmbeddedTestFontA",
"EmbeddedTestFontA ",
" EmbeddedTestFontA ",
" embeddedTESTFonta",
];

public function Test() {
stage.scaleMode = "noScale";

for each (var embedded in [false, true]) {
var fontLists = embedded ? fontListsEmbedded : fontListsDevice;
for each (var fontList in fontLists) {
testFontListCss(embedded, fontList);
testFontListFormat(embedded, fontList);
}
}
}

function testFontListCss(embedded: Boolean, fontList: String) {
trace("Testing CSS font list fallback:");
trace(" Embedded? = " + embedded);
trace(" Font list? = " + fontList);

var style: StyleSheet = new StyleSheet();

var classFontList:Object = new Object();
classFontList.fontFamily = fontList;
classFontList.fontSize = 20;
style.setStyle(".fontlist", classFontList);

var text: TextField = new TextField();
text.embedFonts = embedded;
text.styleSheet = style;

text.width = 100;
text.height = 50;
text.y = nextY;
nextY += text.height;
text.text = "<span class='fontlist'>abc</span>";

addChild(text);

traceChars(text);
}

function testFontListFormat(embedded: Boolean, fontList: String) {
trace("Testing TextFormat font list fallback:");
trace(" Embedded? = " + embedded);
trace(" Font list? = " + fontList);

var tf: TextFormat = new TextFormat(fontList, 20);
var text: TextField = new TextField();
text.embedFonts = embedded;
text.defaultTextFormat = tf;

text.width = 100;
text.height = 50;
text.y = nextY;
nextY += text.height;
text.text = "abc";

addChild(text);

traceChars(text);
}

private function traceChars(text: TextField) {
traceChar(text, 0);
traceChar(text, 1);
traceChar(text, 2);
}

private function traceChar(text: TextField, i: int) {
try {
var ch: Number = text.getCharBoundaries(i).width;
if (ch == 32) {
trace(" Char " + i + " is TestFontA");
}
if (ch == 30) {
trace(" Char " + i + " is TestFontB");
}
} catch(e) {}
}
}
}
88 changes: 88 additions & 0 deletions tests/tests/swfs/fonts/device_font_list/TestFontA.sfd
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
SplineFontDB: 3.2
FontName: TestFontA
FullName: TestFontA
FamilyName: TestFontA
Weight: Regular
Copyright: Copyright (c) 2024, Kamil Jarosz
UComments: "2024-7-24: Created with FontForge (http://fontforge.org)"
Version: 001.000
ItalicAngle: 0
UnderlinePosition: -76
UnderlineWidth: 38
Ascent: 800
Descent: 200
InvalidEm: 0
LayerCount: 2
Layer: 0 0 "Back" 1
Layer: 1 0 "Fore" 0
XUID: [1021 253 198287149 6396829]
StyleMap: 0x0000
FSType: 0
OS2Version: 0
OS2_WeightWidthSlopeOnly: 0
OS2_UseTypoMetrics: 1
CreationTime: 1721856925
ModificationTime: 1737156144
PfmFamily: 17
TTFWeight: 400
TTFWidth: 5
LineGap: 100
VLineGap: 0
OS2TypoAscent: 0
OS2TypoAOffset: 1
OS2TypoDescent: 0
OS2TypoDOffset: 1
OS2TypoLinegap: 100
OS2WinAscent: 0
OS2WinAOffset: 1
OS2WinDescent: 0
OS2WinDOffset: 1
HheadAscent: 0
HheadAOffset: 1
HheadDescent: 0
HheadDOffset: 1
OS2Vendor: 'PfEd'
MarkAttachClasses: 1
DEI: 91125
Encoding: ISO8859-1
UnicodeInterp: none
NameList: AGL For New Fonts
DisplaySize: -48
AntiAlias: 1
FitToEm: 0
WinInfo: 0 30 10
BeginPrivate: 0
EndPrivate
BeginChars: 256 2

StartChar: a
Encoding: 97 97 0
Width: 1600
Flags: HW
LayerCount: 2
Fore
SplineSet
0 800 m 1
1600 800 l 1
1600 0 l 1
0 0 l 1
0 800 l 1
EndSplineSet
EndChar

StartChar: c
Encoding: 99 99 1
Width: 1600
Flags: HW
LayerCount: 2
Fore
SplineSet
0 800 m 1
1600 800 l 1
1600 0 l 1
0 0 l 1
0 800 l 1
EndSplineSet
EndChar
EndChars
EndSplineFont
Binary file not shown.
Loading
Loading