fix(core): improve CJK font fallback loading

- Allow CJK fallback loading to use local variable system fonts
- Add Windows CJK fallback families
- Harden CanvasKit WASM asset resolution and serving
This commit is contained in:
Tem-man 2026-05-05 23:39:39 +08:00 committed by GitHub
parent c69e3884cf
commit 1e2df31636
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 89 additions and 24 deletions

View file

@ -1,3 +1,4 @@
/// <reference types="vite/client" />
import CanvasKitInit, { type CanvasKit } from 'canvaskit-wasm'
import { IS_BROWSER } from './constants'
@ -12,8 +13,10 @@ export async function getCanvasKit(options?: CanvasKitOptions): Promise<CanvasKi
if (instance) return instance
const defaultLocate = (file: string) => {
if (IS_BROWSER) return `/${file}`
return file
if (!IS_BROWSER) return file
const base = 'env' in import.meta ? import.meta.env.BASE_URL : '/'
const prefix = base === '/' ? '' : base.replace(/\/$/, '')
return `${prefix}/${file}`
}
instance = await CanvasKitInit({

View file

@ -25,9 +25,8 @@ export const CANVAS_BG_COLOR_DARK = { r: 0.173, g: 0.173, b: 0.173, a: 1 } satis
*/
export function getDefaultCanvasBgColor(): Color {
if (IS_BROWSER) {
const env = (import.meta as ImportMeta & { env?: { DEV?: boolean } }).env
const params = new URLSearchParams(window.location.search)
if (env?.DEV === true && params.has('test')) {
if ('env' in import.meta && import.meta.env.DEV && params.has('test')) {
return CANVAS_BG_COLOR
}
}
@ -331,10 +330,12 @@ export const CJK_FALLBACK_FAMILIES_MACOS = [
export const CJK_FALLBACK_FAMILIES_WINDOWS = [
'Microsoft YaHei',
'Microsoft YaHei UI',
'Microsoft JhengHei',
'Yu Gothic',
'Malgun Gothic',
'SimHei'
'SimHei',
'SimSun'
]
export const CJK_FALLBACK_FAMILIES_LINUX = [

View file

@ -105,7 +105,9 @@ async function fetchGoogleFontFiles(family: string): Promise<Record<string, stri
}
if (!response.ok) return retryWithNormalizedFamily(family)
const data = (await response.json()) as { items?: Array<{ files?: Record<string, string> }> }
const data = (await response.json()) as {
items?: Array<{ files?: Record<string, string> }>
}
const files = data.items?.[0]?.files
if (!files) return retryWithNormalizedFamily(family)
@ -135,7 +137,13 @@ async function fetchGoogleFont(family: string, style: string): Promise<ArrayBuff
return response.arrayBuffer()
}
async function findLocalFont(family: string, style?: string): Promise<ArrayBuffer | null> {
type FindLocalFontOptions = { allowVariable?: boolean }
async function findLocalFont(
family: string,
style?: string,
options: FindLocalFontOptions = {}
): Promise<ArrayBuffer | null> {
if (!IS_BROWSER || !window.queryLocalFonts) return null
try {
const fonts = await window.queryLocalFonts()
@ -153,9 +161,10 @@ async function findLocalFont(family: string, style?: string): Promise<ArrayBuffe
if (!match) return null
const blob: Blob = await match.blob()
const buffer = await blob.arrayBuffer()
// Variable fonts (fvar table) cause CanvasKit to render all text at the
// default weight. Skip them — Google Fonts serves per-weight static files.
if (isVariableFont(buffer)) return null
// Variable fonts (fvar table) can skew weight for Latin UI fonts. CJK system
// faces (e.g. Microsoft YaHei) are often variable — still prefer them over
// missing glyphs (tofu) when explicitly loading CJK fallbacks.
if (!options.allowVariable && isVariableFont(buffer)) return null
return buffer
} catch (e) {
console.warn(`Local font access failed for "${family}" ${style ?? ''}:`, e)
@ -330,15 +339,16 @@ export async function ensureCJKFallback(): Promise<string[]> {
if (cjkFallbackPromise) return cjkFallbackPromise
cjkFallbackPromise = (async () => {
// Try local system fonts first
// Try local system fonts first (allow variable fonts — common for 微软雅黑 / PingFang).
for (const family of getCJKCandidates()) {
const buffer = await findLocalFont(family)
const buffer = await findLocalFont(family, undefined, {
allowVariable: true
})
if (buffer && registerAndCache(family, 'Regular', buffer)) {
cjkFallbackFamilies.push(family)
}
}
// Load all CJK Google Fonts in parallel for full coverage
if (cjkFallbackFamilies.length === 0) {
const results = await Promise.allSettled(
CJK_GOOGLE_FONTS.map(async (family) => {

5
public/_headers Normal file
View file

@ -0,0 +1,5 @@
/canvaskit.wasm
Content-Type: application/wasm
/canvaskit-webgpu/canvaskit.wasm
Content-Type: application/wasm

View file

@ -1,27 +1,73 @@
import { copyFileSync, existsSync, mkdirSync } from 'fs'
import { copyFileSync, createReadStream, existsSync, mkdirSync } from 'fs'
import { dirname, resolve } from 'node:path'
import type { Connect, ResolvedConfig } from 'vite'
import type { Plugin } from 'vite'
function copyIfMissing(source: string, destination: string) {
if (!existsSync(source) || existsSync(destination)) return
const directory = destination.slice(0, destination.lastIndexOf('/'))
if (directory) mkdirSync(directory, { recursive: true })
copyFileSync(source, destination)
function syncWasmFromNodeModules(root: string, source: string, destination: string) {
const sourcePath = resolve(root, source)
const destinationPath = resolve(root, destination)
if (!existsSync(sourcePath)) {
console.warn(`[copy-canvaskit-wasm] Missing source (run \`bun install\`): ${sourcePath}`)
return
}
mkdirSync(dirname(destinationPath), { recursive: true })
copyFileSync(sourcePath, destinationPath)
}
function serveCanvasKitWasm(root: string): Connect.NextHandleFunction {
return (req, res, next) => {
const pathname = req.url?.split('?')[0] ?? ''
if (pathname !== '/canvaskit.wasm' && pathname !== '/canvaskit-webgpu/canvaskit.wasm') {
next()
return
}
const publicPath = resolve(root, pathname.slice(1))
const fallbackPath =
pathname === '/canvaskit.wasm'
? resolve(root, 'node_modules/canvaskit-wasm/bin/canvaskit.wasm')
: resolve(root, 'packages/core/vendor/canvaskit-webgpu/canvaskit.wasm')
const file = existsSync(publicPath) ? publicPath : fallbackPath
if (!existsSync(file)) {
next()
return
}
res.setHeader('Content-Type', 'application/wasm')
res.setHeader('Cache-Control', 'no-cache')
createReadStream(file).on('error', next).pipe(res)
}
}
export function copyCanvasKitAssetsPlugin(): Plugin {
let root = process.cwd()
return {
name: 'copy-canvaskit-wasm',
enforce: 'pre',
configResolved(config: ResolvedConfig) {
root = config.root
},
buildStart() {
copyIfMissing('node_modules/canvaskit-wasm/bin/canvaskit.wasm', 'public/canvaskit.wasm')
copyIfMissing(
syncWasmFromNodeModules(root, 'node_modules/canvaskit-wasm/bin/canvaskit.wasm', 'public/canvaskit.wasm')
syncWasmFromNodeModules(
root,
'packages/core/vendor/canvaskit-webgpu/canvaskit.wasm',
'public/canvaskit-webgpu/canvaskit.wasm',
'public/canvaskit-webgpu/canvaskit.wasm'
)
copyIfMissing(
syncWasmFromNodeModules(
root,
'packages/core/vendor/canvaskit-webgpu/canvaskit.js',
'public/canvaskit-webgpu/canvaskit.js',
'public/canvaskit-webgpu/canvaskit.js'
)
},
configureServer(server) {
server.middlewares.use(serveCanvasKitWasm(server.config.root))
},
configurePreviewServer(server) {
server.middlewares.use(serveCanvasKitWasm(server.config.root))
}
}
}