-
Notifications
You must be signed in to change notification settings - Fork 839
Sophisticated font fallback #22240
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
base: master
Are you sure you want to change the base?
Sophisticated font fallback #22240
Conversation
|
π€ Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22240/docs/index.html |
1 similar comment
|
π€ Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22240/docs/index.html |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s), but failed to run 1 pipeline(s). |
|
π€ Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22240/docs/index.html |
8e0bbfa to
5775d75
Compare
|
π€ Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22240/docs/index.html |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 16 out of 17 changed files in this pull request and generated 8 comments.
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The System.CommandLine package version 2.0.0-beta4.22272.1 is a pre-release beta version from 2022. Consider using a stable release version if available, as beta versions may have bugs or breaking changes. Check if a stable 2.0+ version has been released since this beta.
| <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> | |
| <PackageReference Include="System.CommandLine" Version="2.0.0" /> |
| ((CompositionTarget)expected.Visual.CompositionTarget)!.FrameRendered += async () => | ||
| { | ||
| var screenshot1 = await UITestHelper.ScreenShot(SUT); | ||
| var screenshot2 = await UITestHelper.ScreenShot(expected); | ||
|
|
||
| var rect = ImageAssert.GetColorBounds(screenshot2, ((SolidColorBrush)DefaultBrushes.TextForegroundBrush).Color); | ||
|
|
||
| // we tolerate a 2 pixels difference between the bitmaps due to font differences | ||
| matched = rect is { Width: > 0, Height: > 0 } && await ImageAssert.AreRenderTargetBitmapsEqualAsync(screenshot1.Bitmap, screenshot2.Bitmap); | ||
| }; |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The FrameRendered event handler is async void, which can lead to unhandled exceptions and test flakiness. The event handler performs async operations (UITestHelper.ScreenShot and ImageAssert.AreRenderTargetBitmapsEqualAsync) but any exceptions thrown won't be properly caught by the test framework. Consider using a TaskCompletionSource or similar mechanism to properly handle async operations in this event handler.
| _fonts = Task.Factory.StartNew(() => | ||
| { | ||
| return Directory.EnumerateFiles("/system/fonts") | ||
| .Select(f => (Path.GetFileName(f), SKTypeface.FromStream(new MemoryStream(File.ReadAllBytes(f))))) |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Loading all font files from /system/fonts into memory at once could cause significant memory pressure, especially on memory-constrained Android devices. The entire contents of each font file are read into a MemoryStream via File.ReadAllBytes. Consider lazy-loading fonts on-demand or using file streams directly with SKTypeface.FromFile if available.
| .Select(f => (Path.GetFileName(f), SKTypeface.FromStream(new MemoryStream(File.ReadAllBytes(f))))) | |
| .Select(f => (Path.GetFileName(f), SKTypeface.FromFile(f))) |
| public async Task<string?> GetFontNameForCodepoint(int codepoint) | ||
| { | ||
| foreach (var (fontName, typeface) in await _fonts) | ||
| { | ||
| if (typeface.ContainsGlyph(codepoint)) | ||
| { | ||
| return fontName; | ||
| } | ||
| } | ||
| return null; |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GetFontNameForCodepoint method performs a linear search through all loaded fonts for each codepoint. This could be slow when many fonts are loaded. Consider building an index/cache mapping codepoint ranges to fonts after loading to improve lookup performance.
| } | ||
|
|
||
| public async Task<SKTypeface?> GetTypefaceForFontName(string fontName, FontWeight weight, FontStretch stretch, FontStyle style) | ||
| => (await _fonts).FirstOrDefault(f => f.fontName.Equals(fontName)).typeface; |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference exception when accessing default tuple element. If no font matches the fontName in the FirstOrDefault call, the tuple will have default values (null for both fontName and typeface), but the code directly accesses .typeface without checking if a match was found. This could return null when a font was actually found but has a null typeface, versus when no font was found at all.
| => (await _fonts).FirstOrDefault(f => f.fontName.Equals(fontName)).typeface; | |
| { | |
| var fonts = await _fonts; | |
| var match = fonts.FirstOrDefault(f => string.Equals(f.fontName, fontName, StringComparison.Ordinal)); | |
| // If no matching font name was found, FirstOrDefault returns the default tuple (null, null). | |
| // In that case, return null to indicate no matching typeface. | |
| if (match.fontName is null) | |
| { | |
| return null; | |
| } | |
| return match.typeface; | |
| } |
| font = locale switch | ||
| { | ||
| "zh-tw" or "zh-mo" => "TraditionalChinese", | ||
| "zh-hk" or "zh-hant-hk" => "TraditionalChineseHK", | ||
| _ when locale.Contains("hant") => "TraditionalChinese", | ||
| _ when locale.Contains("hans") => "SimplifiedChinese", | ||
| _ when locale.Contains("ja") => "Japanese", | ||
| _ when locale.Contains("ko") => "Korean", |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The locale-based CJK font selection uses string.Contains on the locale which could lead to false matches. For example, a locale like "en-hansville" would incorrectly match the "hans" (Simplified Chinese) condition. Use more precise matching or check for specific locale patterns.
| font = locale switch | |
| { | |
| "zh-tw" or "zh-mo" => "TraditionalChinese", | |
| "zh-hk" or "zh-hant-hk" => "TraditionalChineseHK", | |
| _ when locale.Contains("hant") => "TraditionalChinese", | |
| _ when locale.Contains("hans") => "SimplifiedChinese", | |
| _ when locale.Contains("ja") => "Japanese", | |
| _ when locale.Contains("ko") => "Korean", | |
| var localeParts = locale.Split('-', StringSplitOptions.RemoveEmptyEntries); | |
| var languagePart = localeParts.Length > 0 ? localeParts[0] : string.Empty; | |
| bool HasScript(string script) => | |
| localeParts.Any(p => string.Equals(p, script, StringComparison.Ordinal)); | |
| bool IsLanguage(string lang) => | |
| string.Equals(languagePart, lang, StringComparison.Ordinal); | |
| font = locale switch | |
| { | |
| "zh-tw" or "zh-mo" => "TraditionalChinese", | |
| "zh-hk" or "zh-hant-hk" => "TraditionalChineseHK", | |
| _ when HasScript("hant") => "TraditionalChinese", | |
| _ when HasScript("hans") => "SimplifiedChinese", | |
| _ when IsLanguage("ja") => "Japanese", | |
| _ when IsLanguage("ko") => "Korean", |
| if (_missingCodepoints.Count > 0) | ||
| { | ||
| await FetchFontsForMissingCodepoints(); | ||
| } | ||
| else | ||
| { | ||
| _fetchTask = null; | ||
| } |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recursive call without a depth limit could cause stack overflow if _missingCodepoints keeps getting populated during font fetching. Consider adding a maximum recursion depth counter or restructuring to use iteration instead of recursion.
| var (tempFont, task) = GetFont(name, fontSize, weight, stretch, style); | ||
| if (!task.IsCompleted) | ||
| { | ||
| _ = task.ContinueWith(_ => onFontLoaded(), TaskScheduler.FromCurrentSynchronizationContext()); |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential exception if TaskScheduler.FromCurrentSynchronizationContext() returns null. When called from a thread without a SynchronizationContext (e.g., a background thread), FromCurrentSynchronizationContext() will throw an InvalidOperationException. The continuation should handle this case or ensure it's only called from a context with a valid SynchronizationContext.
| _ = task.ContinueWith(_ => onFontLoaded(), TaskScheduler.FromCurrentSynchronizationContext()); | |
| var synchronizationContext = SynchronizationContext.Current; | |
| if (synchronizationContext is not null) | |
| { | |
| _ = task.ContinueWith(_ => onFontLoaded(), TaskScheduler.FromCurrentSynchronizationContext()); | |
| } | |
| else | |
| { | |
| _ = task.ContinueWith(_ => onFontLoaded(), TaskScheduler.Current); | |
| } |
|
π€ Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-22240/wasm-skia-net9/index.html |
|
π€ Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22240/docs/index.html |
|
|
GitHub Issue: closes #20292
PR Type:
What is the current behavior? π€
What is the new behavior? π
PR Checklist β
Please check if your PR fulfills the following requirements:
Screenshots Compare Test Runresults.Other information βΉοΈ