Skip to content
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
14 changes: 14 additions & 0 deletions MiniPdf.Web/MiniPdf.Web.Client/I18n.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ public string T(string key)
["LoadingFont"] = "Downloading CJK font...",
["FontDownloadProgress"] = "Downloading font... {0}% ({1} / {2})",
["FontLoaded"] = "Font loaded successfully",
["CheckingLocalFont"] = "Checking local fonts...",
["LocalFontUsed"] = "Using local font: {0}",
["FontLoadFailed"] = "Font download failed, using default font",
["UploadCustomFont"] = "Custom Font",
["CustomFontHint"] = "Upload your own .ttf / .otf font for better rendering results.",
Expand Down Expand Up @@ -94,6 +96,8 @@ public string T(string key)
["LoadingFont"] = "正在下载 CJK 字体...",
["FontDownloadProgress"] = "正在下载字体... {0}% ({1} / {2})",
["FontLoaded"] = "字体加载成功",
["CheckingLocalFont"] = "正在检测本地字体...",
["LocalFontUsed"] = "使用本地字体:{0}",
["FontLoadFailed"] = "字体下载失败,将使用默认字体",
["UploadCustomFont"] = "自定义字体",
["CustomFontHint"] = "上传您自己的 .ttf / .otf 字体以获得更好的渲染效果。",
Expand Down Expand Up @@ -124,6 +128,8 @@ public string T(string key)
["LoadingFont"] = "正在下載 CJK 字型...",
["FontDownloadProgress"] = "正在下載字型... {0}% ({1} / {2})",
["FontLoaded"] = "字型載入成功",
["CheckingLocalFont"] = "正在偵測本地字型...",
["LocalFontUsed"] = "使用本地字型:{0}",
["FontLoadFailed"] = "字型下載失敗,將使用預設字型",
["UploadCustomFont"] = "自訂字型",
["CustomFontHint"] = "上傳您自己的 .ttf / .otf 字型以獲得更好的渲染效果。",
Expand Down Expand Up @@ -154,6 +160,8 @@ public string T(string key)
["LoadingFont"] = "CJK フォントをダウンロード中...",
["FontDownloadProgress"] = "フォントをダウンロード中... {0}% ({1} / {2})",
["FontLoaded"] = "フォントの読み込みが完了しました",
["CheckingLocalFont"] = "ローカルフォントを確認中...",
["LocalFontUsed"] = "ローカルフォントを使用: {0}",
["FontLoadFailed"] = "フォントのダウンロードに失敗しました。デフォルトフォントを使用します",
["UploadCustomFont"] = "カスタムフォント",
["CustomFontHint"] = ".ttf / .otf フォントをアップロードして、より良いレンダリング結果を得ることができます。",
Expand Down Expand Up @@ -184,6 +192,8 @@ public string T(string key)
["LoadingFont"] = "CJK 글꼴 다운로드 중...",
["FontDownloadProgress"] = "글꼴 다운로드 중... {0}% ({1} / {2})",
["FontLoaded"] = "글꼴 로드 완료",
["CheckingLocalFont"] = "로컬 글꼴 확인 중...",
["LocalFontUsed"] = "로컬 글꼴 사용: {0}",
["FontLoadFailed"] = "글꼴 다운로드 실패, 기본 글꼴을 사용합니다",
["UploadCustomFont"] = "사용자 정의 글꼴",
["CustomFontHint"] = ".ttf / .otf 글꼴을 업로드하여 더 나은 렌더링 결과를 얻을 수 있습니다.",
Expand Down Expand Up @@ -214,6 +224,8 @@ public string T(string key)
["LoadingFont"] = "Téléchargement de la police CJK...",
["FontDownloadProgress"] = "Téléchargement de la police... {0}% ({1} / {2})",
["FontLoaded"] = "Police chargée avec succès",
["CheckingLocalFont"] = "Vérification des polices locales...",
["LocalFontUsed"] = "Police locale utilisée : {0}",
["FontLoadFailed"] = "Échec du téléchargement de la police, utilisation de la police par défaut",
["UploadCustomFont"] = "Police personnalisée",
["CustomFontHint"] = "Téléchargez votre propre police .ttf / .otf pour de meilleurs résultats de rendu.",
Expand Down Expand Up @@ -244,6 +256,8 @@ public string T(string key)
["LoadingFont"] = "Download del font CJK in corso...",
["FontDownloadProgress"] = "Download del font in corso... {0}% ({1} / {2})",
["FontLoaded"] = "Font caricato con successo",
["CheckingLocalFont"] = "Verifica dei font locali in corso...",
["LocalFontUsed"] = "Font locale in uso: {0}",
["FontLoadFailed"] = "Download del font non riuscito, verrà usato il font predefinito",
["UploadCustomFont"] = "Font personalizzato",
["CustomFontHint"] = "Carica il tuo font .ttf / .otf per risultati di rendering migliori.",
Expand Down
77 changes: 53 additions & 24 deletions MiniPdf.Web/MiniPdf.Web.Client/Pages/Converter.razor
Original file line number Diff line number Diff line change
Expand Up @@ -233,43 +233,72 @@
if (!_fontLoaded)
{
isFontLoading = true;
fontStatus = L.T("LoadingFont");
fontStatus = L.T("CheckingLocalFont");
StateHasChanged();
await Task.Yield();

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 { }

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}");
                    }


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;
Comment on lines +251 to +255
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");
}
}
Comment on lines 240 to 302

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.

catch
{
Expand Down
43 changes: 43 additions & 0 deletions MiniPdf.Web/MiniPdf.Web.Client/wwwroot/js/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,46 @@ window.downloadFile = function (fileName, contentType, byteArray) {
document.body.removeChild(a);
URL.revokeObjectURL(url);
};

// Local Font Access API support (Chrome 103+)
// Returns the font-family name of the first matching CJK font, or null.
// The font blob is cached so getLocalFontStream() can return it immediately after.
let _localFontBlob = null;

window.tryLoadLocalFontMeta = async function () {
if (!('queryLocalFonts' in window)) return null;
try {
const fonts = await window.queryLocalFonts();
const preferred = [
// Windows
'Microsoft YaHei', 'Microsoft YaHei UI',
'SimSun', 'SimHei', 'FangSong', 'KaiTi',
// macOS / iOS
'PingFang SC', 'Heiti SC', 'STHeiti', 'STSong',
// Japanese (Windows / macOS)
'Meiryo', 'Yu Gothic', 'MS Gothic', 'Hiragino Sans',
// Korean (Windows / macOS)
'Malgun Gothic', 'Apple SD Gothic Neo', 'Gulim', 'Dotum',
// Google Noto / open-source
'Noto Sans CJK SC', 'Noto Sans SC', 'Source Han Sans SC',
];
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;
}
Comment on lines +35 to +40

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;
                }
            }
        }

if (!found) return null;
_localFontBlob = await found.blob();
return found.family;
} catch {
return null;
}
};

// Returns a ReadableStream for the cached font blob (call after tryLoadLocalFontMeta succeeds).
window.getLocalFontStream = function () {
const blob = _localFontBlob;
_localFontBlob = null; // release reference
return blob ? blob.stream() : null;
};
Loading