fix(tauri): prevent Windows font loading crashes (#497)

* fix(tauri): prevent Windows font loading crashes

- Return native font files over binary Tauri IPC instead of JSON byte arrays

- Resolve desktop script fallbacks without parsing large system fonts in JavaScript

* fix(core): restore quality checks

- Use the shared Vector primitive for render-bound offsets

- Remove unsupported SLICE handling from SceneGraph rescaling

- Drop an unused generated-text test binding

* chore: address font fix review

- Place the release note under the Fixed heading

- Name fallback resolution options and use the shared Tauri constant
This commit is contained in:
Danila Poyarkov 2026-08-12 20:09:04 +03:00 committed by GitHub
parent 3102a60f91
commit a8cf8eb412
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 67 additions and 14 deletions

View file

@ -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)

View file

@ -112,8 +112,13 @@ fn load_system_font_blocking(family: String, style: String) -> Result<Vec<u8>, S
}
#[tauri::command]
pub async fn load_system_font(family: String, style: String) -> Result<Vec<u8>, 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<tauri::ipc::Response, String> {
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))
}

View file

@ -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<FontFallbackScript>()
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<FontFallbackScript>()
if (textNeedsFallbackScript(node, 'arabic')) scripts.add('arabic')

View file

@ -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<FontFallbackScript>()
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)
}

View file

@ -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<ArrayB
if (!isTauri()) return null
try {
const { invoke } = await import('@tauri-apps/api/core')
const data = await invoke<number[] | null>('load_system_font', { family, style })
if (!data?.length) return null
return new Uint8Array(data).buffer
const data = await invoke<ArrayBuffer>('load_system_font', { family, style })
if (data.byteLength === 0) return null
return data
} catch {
return null
}

View file

@ -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)

View file

@ -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,

View file

@ -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')