Skip to content

Conversation

@ramezgerges
Copy link
Contributor

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:

Other information ℹ️

Copilot AI review requested due to automatic review settings January 5, 2026 12:22
@github-actions github-actions bot added platform/wasm 🌐 Categorizes an issue or PR as relevant to the WebAssembly platform platform/android πŸ€– Categorizes an issue or PR as relevant to the Android platform area/skia ✏️ Categorizes an issue or PR as relevant to Skia labels Jan 5, 2026
@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22240/docs/index.html

1 similar comment
@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22240/docs/index.html

Copy link
Contributor

Copilot AI left a 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.

@ramezgerges
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s), but failed to run 1 pipeline(s).

@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22240/docs/index.html

@ramezgerges ramezgerges force-pushed the sophisticated_font_fallback branch from 8e0bbfa to 5775d75 Compare January 8, 2026 12:48
@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22240/docs/index.html

Copilot AI review requested due to automatic review settings January 9, 2026 17:31
Copy link
Contributor

Copilot AI left a 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" />
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0" />

Copilot uses AI. Check for mistakes.
Comment on lines +227 to +236
((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);
};
Copy link

Copilot AI Jan 9, 2026

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.

Copilot uses AI. Check for mistakes.
_fonts = Task.Factory.StartNew(() =>
{
return Directory.EnumerateFiles("/system/fonts")
.Select(f => (Path.GetFileName(f), SKTypeface.FromStream(new MemoryStream(File.ReadAllBytes(f)))))
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
.Select(f => (Path.GetFileName(f), SKTypeface.FromStream(new MemoryStream(File.ReadAllBytes(f)))))
.Select(f => (Path.GetFileName(f), SKTypeface.FromFile(f)))

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +37
public async Task<string?> GetFontNameForCodepoint(int codepoint)
{
foreach (var (fontName, typeface) in await _fonts)
{
if (typeface.ContainsGlyph(codepoint))
{
return fontName;
}
}
return null;
Copy link

Copilot AI Jan 9, 2026

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.

Copilot uses AI. Check for mistakes.
}

public async Task<SKTypeface?> GetTypefaceForFontName(string fontName, FontWeight weight, FontStretch stretch, FontStyle style)
=> (await _fonts).FirstOrDefault(f => f.fontName.Equals(fontName)).typeface;
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
=> (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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +114
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",
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
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",

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +142
if (_missingCodepoints.Count > 0)
{
await FetchFontsForMissingCodepoints();
}
else
{
_fetchTask = null;
}
Copy link

Copilot AI Jan 9, 2026

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.

Copilot uses AI. Check for mistakes.
var (tempFont, task) = GetFont(name, fontSize, weight, stretch, style);
if (!task.IsCompleted)
{
_ = task.ContinueWith(_ => onFontLoaded(), TaskScheduler.FromCurrentSynchronizationContext());
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
_ = 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);
}

Copilot uses AI. Check for mistakes.
@unodevops
Copy link
Contributor

πŸ€– 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

@unodevops
Copy link
Contributor

πŸ€– Your Docs stage site is ready! Visit it here: https://unodocsprstaging.z13.web.core.windows.net/pr-22240/docs/index.html

@unodevops
Copy link
Contributor

⚠️⚠️ The build 190710 has failed on Uno.UI - CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/skia ✏️ Categorizes an issue or PR as relevant to Skia platform/android πŸ€– Categorizes an issue or PR as relevant to the Android platform platform/wasm 🌐 Categorizes an issue or PR as relevant to the WebAssembly platform

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Skia] Unicode (chinese, georgian etc.) character are not rendering properly

2 participants