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:
parent
c69e3884cf
commit
1e2df31636
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
5
public/_headers
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/canvaskit.wasm
|
||||
Content-Type: application/wasm
|
||||
|
||||
/canvaskit-webgpu/canvaskit.wasm
|
||||
Content-Type: application/wasm
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue