diff --git a/MiniPdf.Web/MiniPdf.Web.Client/I18n.cs b/MiniPdf.Web/MiniPdf.Web.Client/I18n.cs index 8d09056d..9a1e0344 100644 --- a/MiniPdf.Web/MiniPdf.Web.Client/I18n.cs +++ b/MiniPdf.Web/MiniPdf.Web.Client/I18n.cs @@ -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.", @@ -94,6 +96,8 @@ public string T(string key) ["LoadingFont"] = "正在下载 CJK 字体...", ["FontDownloadProgress"] = "正在下载字体... {0}% ({1} / {2})", ["FontLoaded"] = "字体加载成功", + ["CheckingLocalFont"] = "正在检测本地字体...", + ["LocalFontUsed"] = "使用本地字体:{0}", ["FontLoadFailed"] = "字体下载失败,将使用默认字体", ["UploadCustomFont"] = "自定义字体", ["CustomFontHint"] = "上传您自己的 .ttf / .otf 字体以获得更好的渲染效果。", @@ -124,6 +128,8 @@ public string T(string key) ["LoadingFont"] = "正在下載 CJK 字型...", ["FontDownloadProgress"] = "正在下載字型... {0}% ({1} / {2})", ["FontLoaded"] = "字型載入成功", + ["CheckingLocalFont"] = "正在偵測本地字型...", + ["LocalFontUsed"] = "使用本地字型:{0}", ["FontLoadFailed"] = "字型下載失敗,將使用預設字型", ["UploadCustomFont"] = "自訂字型", ["CustomFontHint"] = "上傳您自己的 .ttf / .otf 字型以獲得更好的渲染效果。", @@ -154,6 +160,8 @@ public string T(string key) ["LoadingFont"] = "CJK フォントをダウンロード中...", ["FontDownloadProgress"] = "フォントをダウンロード中... {0}% ({1} / {2})", ["FontLoaded"] = "フォントの読み込みが完了しました", + ["CheckingLocalFont"] = "ローカルフォントを確認中...", + ["LocalFontUsed"] = "ローカルフォントを使用: {0}", ["FontLoadFailed"] = "フォントのダウンロードに失敗しました。デフォルトフォントを使用します", ["UploadCustomFont"] = "カスタムフォント", ["CustomFontHint"] = ".ttf / .otf フォントをアップロードして、より良いレンダリング結果を得ることができます。", @@ -184,6 +192,8 @@ public string T(string key) ["LoadingFont"] = "CJK 글꼴 다운로드 중...", ["FontDownloadProgress"] = "글꼴 다운로드 중... {0}% ({1} / {2})", ["FontLoaded"] = "글꼴 로드 완료", + ["CheckingLocalFont"] = "로컬 글꼴 확인 중...", + ["LocalFontUsed"] = "로컬 글꼴 사용: {0}", ["FontLoadFailed"] = "글꼴 다운로드 실패, 기본 글꼴을 사용합니다", ["UploadCustomFont"] = "사용자 정의 글꼴", ["CustomFontHint"] = ".ttf / .otf 글꼴을 업로드하여 더 나은 렌더링 결과를 얻을 수 있습니다.", @@ -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.", @@ -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.", diff --git a/MiniPdf.Web/MiniPdf.Web.Client/Pages/Converter.razor b/MiniPdf.Web/MiniPdf.Web.Client/Pages/Converter.razor index 0b6ba3dc..8ac82b0f 100644 --- a/MiniPdf.Web/MiniPdf.Web.Client/Pages/Converter.razor +++ b/MiniPdf.Web/MiniPdf.Web.Client/Pages/Converter.razor @@ -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("tryLoadLocalFontMeta"); } catch { } + + if (localFontFamily is not null) { - fontMs.Write(buffer, 0, read); - totalRead += read; - if (totalBytes > 0) + var jsStreamRef = await JS.InvokeAsync("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"); + } } catch { diff --git a/MiniPdf.Web/MiniPdf.Web.Client/wwwroot/js/download.js b/MiniPdf.Web/MiniPdf.Web.Client/wwwroot/js/download.js index 7e8fe60c..07099899 100644 --- a/MiniPdf.Web/MiniPdf.Web.Client/wwwroot/js/download.js +++ b/MiniPdf.Web/MiniPdf.Web.Client/wwwroot/js/download.js @@ -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; + } + 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; +};