Skip to content

Add local font checking and loading functionality for improved font m…#59

Merged
shps951023 merged 1 commit intomainfrom
v3
Mar 19, 2026
Merged

Add local font checking and loading functionality for improved font m…#59
shps951023 merged 1 commit intomainfrom
v3

Conversation

@shps951023
Copy link
Member

…anagement

Copilot AI review requested due to automatic review settings March 19, 2026 13:40
@shps951023 shps951023 merged commit 8942025 into main Mar 19, 2026
2 checks passed
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's font management by introducing the ability to leverage locally installed fonts. This change aims to provide a smoother and faster experience for users, particularly those requiring CJK fonts, by reducing network requests and improving rendering efficiency. The system now intelligently checks for local font availability before resorting to downloading, ensuring optimal font delivery.

Highlights

  • Local Font Access API Integration: Implemented support for the Local Font Access API (available in Chrome 103+) to detect and utilize locally installed fonts, improving performance and user experience by reducing the need for font downloads.
  • Prioritized Font Loading: Modified the font loading mechanism to first attempt loading a suitable CJK font from the user's local system. If no local font is found or accessible, it gracefully falls back to downloading the NotoSansSC font.
  • Internationalization Updates: Added new localization strings across multiple languages (English, Simplified Chinese, Traditional Chinese, Japanese, Korean, French, Italian) to inform users about local font checking and usage status.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds a great enhancement by attempting to use local fonts before downloading them, which can improve performance and user experience. The implementation is mostly solid, but I have a few suggestions to improve performance, maintainability, and robustness. In Converter.razor, I've suggested refactoring the font loading logic into a separate method to improve readability, and to add logging for exceptions that are currently being silently ignored. In download.js, I've proposed an optimization for the font-finding algorithm to make it more performant, especially for users with many fonts installed.

Comment on lines 240 to 302
try
{
var request = new HttpRequestMessage(HttpMethod.Get, "fonts/NotoSansSC-Regular.ttf");
request.SetBrowserResponseStreamingEnabled(true);
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();

var totalBytes = response.Content.Headers.ContentLength ?? -1;
using var contentStream = await response.Content.ReadAsStreamAsync();
using var fontMs = new MemoryStream();
var buffer = new byte[81920];
long totalRead = 0;
int read;

while ((read = await contentStream.ReadAsync(buffer)) > 0)
// Try Local Font Access API first (Chrome 103+, requires user permission)
string? localFontFamily = null;
try { localFontFamily = await JS.InvokeAsync<string?>("tryLoadLocalFontMeta"); } catch { }

if (localFontFamily is not null)
{
fontMs.Write(buffer, 0, read);
totalRead += read;
if (totalBytes > 0)
var jsStreamRef = await JS.InvokeAsync<IJSStreamReference?>("getLocalFontStream");
if (jsStreamRef is not null)
{
fontProgress = (int)(totalRead * 100 / totalBytes);
fontStatus = string.Format(L.T("FontDownloadProgress"), fontProgress, FormatSize(totalRead), FormatSize(totalBytes));
await using var fontStream = await jsStreamRef.OpenReadStreamAsync(maxAllowedSize: 50 * 1024 * 1024);
using var fontMs = new MemoryStream();
await fontStream.CopyToAsync(fontMs);
MiniSoftware.MiniPdf.RegisterFont(localFontFamily, fontMs.ToArray());
fontLoadSuccess = true;
fontStatus = "✓ " + string.Format(L.T("LocalFontUsed"), localFontFamily);
}
else
{
fontStatus = $"{L.T("LoadingFont")} ({FormatSize(totalRead)})";
localFontFamily = null; // stream unexpectedly null, fall through to download
}
StateHasChanged();
}

MiniSoftware.MiniPdf.RegisterFont("NotoSansSC", fontMs.ToArray());
fontLoadSuccess = true;
fontStatus = "✓ " + L.T("FontLoaded");
if (localFontFamily is null)
{
// Fall back to downloading NotoSansSC
fontStatus = L.T("LoadingFont");
StateHasChanged();

var request = new HttpRequestMessage(HttpMethod.Get, "fonts/NotoSansSC-Regular.ttf");
request.SetBrowserResponseStreamingEnabled(true);
using var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();

var totalBytes = response.Content.Headers.ContentLength ?? -1;
using var contentStream = await response.Content.ReadAsStreamAsync();
using var fontMs = new MemoryStream();
var buffer = new byte[81920];
long totalRead = 0;
int read;

while ((read = await contentStream.ReadAsync(buffer)) > 0)
{
fontMs.Write(buffer, 0, read);
totalRead += read;
if (totalBytes > 0)
{
fontProgress = (int)(totalRead * 100 / totalBytes);
fontStatus = string.Format(L.T("FontDownloadProgress"), fontProgress, FormatSize(totalRead), FormatSize(totalBytes));
}
else
{
fontStatus = $"{L.T("LoadingFont")} ({FormatSize(totalRead)})";
}
StateHasChanged();
}

MiniSoftware.MiniPdf.RegisterFont("NotoSansSC", fontMs.ToArray());
fontLoadSuccess = true;
fontStatus = "✓ " + L.T("FontLoaded");
}
}

Choose a reason for hiding this comment

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

medium

The font loading logic within this try-catch block has grown quite large and complex. To improve the readability and maintainability of the ConvertFile method, consider extracting this font loading logic into a new private async method, for example EnsureFontLoadedAsync(). The ConvertFile method would then just need to await this new method.

while ((read = await contentStream.ReadAsync(buffer)) > 0)
// Try Local Font Access API first (Chrome 103+, requires user permission)
string? localFontFamily = null;
try { localFontFamily = await JS.InvokeAsync<string?>("tryLoadLocalFontMeta"); } catch { }

Choose a reason for hiding this comment

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

medium

Swallowing exceptions with an empty catch block can hide bugs and make debugging difficult. If the JavaScript interop call fails for an unexpected reason, it will fail silently. It's better to log the exception to the developer console to aid in debugging, while still allowing the code to fall back to the font download behavior.

                    try
                    {
                        localFontFamily = await JS.InvokeAsync<string?>("tryLoadLocalFontMeta");
                    }
                    catch (Exception ex)
                    {
                        // This is expected to fail if the API is not supported or permission is denied.
                        // Log for debugging and fall back to downloading.
                        Console.WriteLine($"Could not query local fonts: {ex.Message}");
                    }

Comment on lines +35 to +40
let found = null;
for (const family of preferred) {
found = fonts.find(f => f.family === family && (f.style === 'Regular' || f.style === 'Normal'));
if (!found) found = fonts.find(f => f.family === family);
if (found) break;
}

Choose a reason for hiding this comment

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

medium

The current implementation for finding a preferred font can be inefficient. It iterates over the entire fonts array up to twice for each font family in the preferred list. For users with many fonts, this could be slow. A more performant approach is to first group the available fonts by family name and then iterate through the preferred list. This reduces the algorithmic complexity and will perform better.

        const familyMap = new Map();
        for (const font of fonts) {
            if (!familyMap.has(font.family)) {
                familyMap.set(font.family, []);
            }
            familyMap.get(font.family).push(font);
        }

        let found = null;
        for (const family of preferred) {
            const familyFonts = familyMap.get(family);
            if (familyFonts) {
                found = familyFonts.find(f => f.style === 'Regular' || f.style === 'Normal') || familyFonts[0];
                if (found) {
                    break;
                }
            }
        }

Copy link

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.

Pull request overview

Adds client-side local font detection/loading to reduce the need to download bundled CJK fonts during conversions in the Blazor WebAssembly app.

Changes:

  • Introduces Local Font Access API probing in download.js and exposes JS helpers to return a cached font stream.
  • Updates the converter page to prefer a locally available CJK font (when permitted) and fall back to downloading NotoSansSC-Regular.ttf.
  • Adds new i18n strings for “checking local fonts” and “using local font” across supported languages.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
MiniPdf.Web/MiniPdf.Web.Client/wwwroot/js/download.js Adds Local Font Access API helpers to locate a preferred local CJK font and provide a stream to .NET.
MiniPdf.Web/MiniPdf.Web.Client/Pages/Converter.razor Uses JS helpers to load/register a local font first, otherwise downloads and registers the bundled Noto Sans SC font.
MiniPdf.Web/MiniPdf.Web.Client/I18n.cs Adds new translation keys for local-font checking/usage status messages.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +251 to +255
await using var fontStream = await jsStreamRef.OpenReadStreamAsync(maxAllowedSize: 50 * 1024 * 1024);
using var fontMs = new MemoryStream();
await fontStream.CopyToAsync(fontMs);
MiniSoftware.MiniPdf.RegisterFont(localFontFamily, fontMs.ToArray());
fontLoadSuccess = true;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants