fix(canvas): render CJK fallback text as outlines
This commit is contained in:
parent
2c171139c1
commit
3efc7de149
|
|
@ -606,6 +606,12 @@ function drawOutlinedText(r: SkiaRenderer, canvas: Canvas, node: SceneNode): boo
|
|||
return true
|
||||
}
|
||||
|
||||
const CJK_TEXT_PATTERN = /[\u3040-\u30ff\u3400-\u9fff\uf900-\ufaff\uac00-\ud7af]/u
|
||||
|
||||
function shouldRenderCJKAsOutline(node: SceneNode): boolean {
|
||||
return CJK_TEXT_PATTERN.test(node.text)
|
||||
}
|
||||
|
||||
function drawGradientText(
|
||||
r: SkiaRenderer,
|
||||
canvas: Canvas,
|
||||
|
|
@ -666,7 +672,10 @@ export function renderText(r: SkiaRenderer, canvas: Canvas, node: SceneNode, fil
|
|||
canvas.restore()
|
||||
return
|
||||
}
|
||||
if (shouldRenderTextAsOutline(fill) && drawOutlinedText(r, canvas, node)) {
|
||||
if (
|
||||
(shouldRenderTextAsOutline(fill) || shouldRenderCJKAsOutline(node)) &&
|
||||
drawOutlinedText(r, canvas, node)
|
||||
) {
|
||||
canvas.restore()
|
||||
return
|
||||
}
|
||||
|
|
|
|||
121
tests/e2e/fonts/cjk-rendering-visual.spec.ts
Normal file
121
tests/e2e/fonts/cjk-rendering-visual.spec.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { existsSync, readFileSync } from 'node:fs'
|
||||
|
||||
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
|
||||
|
||||
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
|
||||
const appleGothic = '/System/Library/Fonts/Supplemental/AppleGothic.ttf'
|
||||
|
||||
test.skip(
|
||||
process.platform !== 'darwin' || !existsSync(appleGothic),
|
||||
'CJK visual snapshot uses macOS AppleGothic for Hangul coverage'
|
||||
)
|
||||
|
||||
async function expectCanvas(name: string) {
|
||||
editor.canvas.assertNoErrors()
|
||||
const buffer = await editor.canvas.canvas.screenshot()
|
||||
expect(buffer).toMatchSnapshot(`${name}.png`)
|
||||
}
|
||||
|
||||
test('renders Simplified Chinese, Traditional Chinese, Japanese, and Korean fallback text', async () => {
|
||||
await editor.page.route('**/__test-fonts/apple-gothic.ttf', async (route) => {
|
||||
await route.fulfill({ body: readFileSync(appleGothic), contentType: 'font/ttf' })
|
||||
})
|
||||
|
||||
const smoke = await editor.page.evaluate(async () => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const pageId = store.state.currentPageId
|
||||
const { fontManager } = await import('/packages/core/src/text/fonts.ts')
|
||||
|
||||
const [notoResponse, appleGothicResponse] = await Promise.all([
|
||||
fetch('/tests/fixtures/fonts/NotoSansSC-Regular.ttf'),
|
||||
fetch('/__test-fonts/apple-gothic.ttf')
|
||||
])
|
||||
if (!notoResponse.ok) throw new Error(`Failed to load Noto Sans SC: ${notoResponse.status}`)
|
||||
if (!appleGothicResponse.ok) {
|
||||
throw new Error(`Failed to load AppleGothic: ${appleGothicResponse.status}`)
|
||||
}
|
||||
|
||||
fontManager.markLoaded('Noto Sans SC', 'Regular', await notoResponse.arrayBuffer())
|
||||
fontManager.markLoaded('AppleGothic', 'Regular', await appleGothicResponse.arrayBuffer())
|
||||
fontManager.setCJKFallbackFamily('Noto Sans SC')
|
||||
fontManager.setCJKFallbackFamily('AppleGothic')
|
||||
|
||||
const lines = [
|
||||
{ label: 'Simplified Chinese', text: '你好世界' },
|
||||
{ label: 'Traditional Chinese', text: '繁體中文' },
|
||||
{ label: 'Japanese', text: '日本語かなカナ' },
|
||||
{ label: 'Korean', text: '안녕하세요' }
|
||||
]
|
||||
|
||||
store.graph.createNode('FRAME', pageId, {
|
||||
name: 'CJK rendering visual backdrop',
|
||||
x: 64,
|
||||
y: 52,
|
||||
width: 720,
|
||||
height: 312,
|
||||
cornerRadius: 20,
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.97, g: 0.98, b: 1, a: 1 }, visible: true, opacity: 1 }
|
||||
],
|
||||
strokes: [
|
||||
{
|
||||
color: { r: 0.78, g: 0.84, b: 0.95, a: 1 },
|
||||
weight: 1,
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
align: 'INSIDE'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
for (const [index, line] of lines.entries()) {
|
||||
const y = 84 + index * 64
|
||||
store.graph.createNode('TEXT', pageId, {
|
||||
name: `${line.label} label`,
|
||||
x: 96,
|
||||
y,
|
||||
width: 210,
|
||||
height: 28,
|
||||
text: line.label,
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 17,
|
||||
fontWeight: 600,
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.25, g: 0.32, b: 0.45, a: 1 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
store.graph.createNode('TEXT', pageId, {
|
||||
name: `${line.label} sample`,
|
||||
x: 328,
|
||||
y: y - 4,
|
||||
width: 360,
|
||||
height: 42,
|
||||
text: line.text,
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 32,
|
||||
textAutoResize: 'HEIGHT',
|
||||
fills: [
|
||||
{ type: 'SOLID', color: { r: 0.04, g: 0.06, b: 0.12, a: 1 }, visible: true, opacity: 1 }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
await store.loadFontsForNodes(store.graph.getPages().flatMap((page) => page.childIds))
|
||||
store.clearSelection()
|
||||
store.requestRender()
|
||||
|
||||
return {
|
||||
loaded: {
|
||||
noto: fontManager.isStyleLoaded('Noto Sans SC', 'Regular'),
|
||||
appleGothic: fontManager.isStyleLoaded('AppleGothic', 'Regular')
|
||||
},
|
||||
fallbacks: fontManager.getCJKFallbackFamilies()
|
||||
}
|
||||
})
|
||||
|
||||
expect(smoke.loaded).toEqual({ noto: true, appleGothic: true })
|
||||
expect(smoke.fallbacks).toEqual(expect.arrayContaining(['Noto Sans SC', 'AppleGothic']))
|
||||
await editor.canvas.waitForRender()
|
||||
await expectCanvas('cjk-fallback-text-rendering')
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Loading…
Reference in a new issue