diff --git a/CHANGELOG.md b/CHANGELOG.md index c04eb52ea..725f321cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Fixed +- Fixed Windows desktop crashes when loading large system fonts for non-Latin text by using Tauri's binary IPC path and resolving native script fallbacks without parsing full font files in JavaScript. + - Preserve open vector segments when the same vector network also contains filled regions. (#450) - Match Figma Plugin API behavior for `rescale()`, page `backgrounds`, and nullable visual `absoluteRenderBounds`. (#442) - Keep imported Figma instances linked to their remapped source components so later component edits update existing instances. (#385) diff --git a/desktop/src/fonts.rs b/desktop/src/fonts.rs index 96ee9dd8a..e4aed3c8f 100644 --- a/desktop/src/fonts.rs +++ b/desktop/src/fonts.rs @@ -112,8 +112,13 @@ fn load_system_font_blocking(family: String, style: String) -> Result, S } #[tauri::command] -pub async fn load_system_font(family: String, style: String) -> Result, String> { - tauri::async_runtime::spawn_blocking(move || load_system_font_blocking(family, style)) - .await - .map_err(|e| format!("Font load task failed: {e}"))? +pub async fn load_system_font( + family: String, + style: String, +) -> Result { + let data = + tauri::async_runtime::spawn_blocking(move || load_system_font_blocking(family, style)) + .await + .map_err(|e| format!("Font load task failed: {e}"))??; + Ok(tauri::ipc::Response::new(data)) } diff --git a/packages/core/src/text/coverage.ts b/packages/core/src/text/coverage.ts index a7ea77040..7fc760995 100644 --- a/packages/core/src/text/coverage.ts +++ b/packages/core/src/text/coverage.ts @@ -85,6 +85,19 @@ export function textNeedsFallbackScript(node: SceneNode, script: FontFallbackScr return false } +export function textFallbackScriptsWithoutCoverage(node: SceneNode): FontFallbackScript[] { + if (node.type !== 'TEXT') return [] + const scripts = new Set() + let index = 0 + for (const char of node.text) { + const { language } = styleForCharacter(node, index) + const script = fontFallbackScriptForCharacter(char, language) + if (script) scripts.add(script) + index += char.length + } + return [...scripts] +} + export function textNeededFallbackScripts(node: SceneNode): FontFallbackScript[] { const scripts = new Set() if (textNeedsFallbackScript(node, 'arabic')) scripts.add('arabic') diff --git a/packages/core/src/text/resolved-requirements.ts b/packages/core/src/text/resolved-requirements.ts index f6f2db4ff..f4ba2527d 100644 --- a/packages/core/src/text/resolved-requirements.ts +++ b/packages/core/src/text/resolved-requirements.ts @@ -1,12 +1,22 @@ -import { textNeededFallbackScripts } from '#core/text/coverage' +import { textFallbackScriptsWithoutCoverage, textNeededFallbackScripts } from '#core/text/coverage' import type { FontFallbackScript } from '#core/text/fallbacks' import type { GraphFontRequirements } from '#core/text/requirements' -export function missingGraphFontScripts(requirements: GraphFontRequirements): FontFallbackScript[] { +export interface MissingGraphFontScriptsOptions { + treatUnknownCoverageAsMissing?: boolean +} + +export function missingGraphFontScripts( + requirements: GraphFontRequirements, + options: MissingGraphFontScriptsOptions = {} +): FontFallbackScript[] { const scripts = new Set() for (const node of requirements.nodes) { if (node.type !== 'TEXT') continue - for (const script of textNeededFallbackScripts(node)) scripts.add(script) + const neededScripts = options.treatUnknownCoverageAsMissing + ? textFallbackScriptsWithoutCoverage(node) + : textNeededFallbackScripts(node) + for (const script of neededScripts) scripts.add(script) } return Array.from(scripts) } diff --git a/src/app/editor/fonts/index.ts b/src/app/editor/fonts/index.ts index 89172977d..1fc3371b9 100644 --- a/src/app/editor/fonts/index.ts +++ b/src/app/editor/fonts/index.ts @@ -22,6 +22,7 @@ import { import { toast } from '@/app/shell/ui' import { isTauri } from '@/app/tauri/env' import { tauriFetch } from '@/app/tauri/http' +import { IS_TAURI } from '@/constants' if (typeof navigator !== 'undefined') { fontManager.setFallbackUserAgent(navigator.userAgent) @@ -177,7 +178,9 @@ export async function ensureGraphFonts( const requirements = collectGraphFontRequirements(graph, nodeIds) const { characters } = requirements await Promise.all(fontKeys.map(([family, style]) => loadFont(family, style, characters))) - const fallbackScripts = missingGraphFontScripts(requirements) + const fallbackScripts = missingGraphFontScripts(requirements, { + treatUnknownCoverageAsMissing: IS_TAURI + }) if (fallbackScripts.length > 0) { const fallbacks = await fontManager.ensureFallbackPack(fallbackScripts, characters) if (Object.values(fallbacks).some((families) => families.length > 0)) { @@ -207,9 +210,9 @@ async function loadSystemFont(family: string, style = 'Regular'): Promise('load_system_font', { family, style }) - if (!data?.length) return null - return new Uint8Array(data).buffer + const data = await invoke('load_system_font', { family, style }) + if (data.byteLength === 0) return null + return data } catch { return null } diff --git a/tests/engine/app/font-loading.test.ts b/tests/engine/app/font-loading.test.ts index b24b08d92..6df567b9b 100644 --- a/tests/engine/app/font-loading.test.ts +++ b/tests/engine/app/font-loading.test.ts @@ -1,6 +1,11 @@ import { describe, expect, test } from 'bun:test' -import { fontManager, type FontFallbackScript } from '@open-pencil/core/text' +import { + collectGraphFontRequirements, + fontManager, + missingGraphFontScripts, + type FontFallbackScript +} from '@open-pencil/core/text' import { SceneGraph } from '@open-pencil/scene-graph' import { ensureGraphFonts } from '@/app/editor/fonts' @@ -9,6 +14,21 @@ import { expectDefined } from '#tests/helpers/assert' import { repoPath } from '#tests/helpers/paths' describe('app font loading', () => { + test('requests platform fallback before parsing large native primary fonts', () => { + const graph = new SceneGraph() + const page = graph.getPages()[0] + const text = graph.createNode('TEXT', page.id, { + text: '현대 소나타', + fontFamily: 'Native Arial', + fontSize: 32 + }) + const requirements = collectGraphFontRequirements(graph, [text.id]) + + expect(missingGraphFontScripts(requirements, { treatUnknownCoverageAsMissing: true })).toEqual([ + 'cjk-kr' + ]) + }) + test('ensureGraphFonts loads fallback packs when loaded primary font misses CJK glyphs', async () => { const interData = await Bun.file(repoPath('public/Inter-Regular.ttf')).arrayBuffer() fontManager.markLoaded('Inter', 'Regular', interData) diff --git a/tests/engine/layout/auto-layout/imported-derived-layout/effective-generated-text.test.ts b/tests/engine/layout/auto-layout/imported-derived-layout/effective-generated-text.test.ts index 3c44cf79b..873c03705 100644 --- a/tests/engine/layout/auto-layout/imported-derived-layout/effective-generated-text.test.ts +++ b/tests/engine/layout/auto-layout/imported-derived-layout/effective-generated-text.test.ts @@ -111,7 +111,7 @@ describe('effective generated FIG text layout', () => { componentId: 'parent', figmaDerivedLayout: { width: 120, height: 80 } }) - const generatedText = graph.createNode('TEXT', parent.id, { + graph.createNode('TEXT', parent.id, { width: 100, height: 20, text: source.text, diff --git a/tests/engine/tauri/fonts.test.ts b/tests/engine/tauri/fonts.test.ts index 3299b1256..12ab3e739 100644 --- a/tests/engine/tauri/fonts.test.ts +++ b/tests/engine/tauri/fonts.test.ts @@ -29,7 +29,7 @@ describe('Tauri font helpers', () => { await mockTauriIPC((cmd, args) => { expect(cmd).toBe('load_system_font') expect(args).toEqual({ family: 'System UI', style: 'Bold Italic' }) - return [1, 2, 3, 4] + return new Uint8Array([1, 2, 3, 4]).buffer }) const { loadFont } = await import('@/app/editor/fonts')