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:
parent
3102a60f91
commit
a8cf8eb412
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue