fix(text): verify visible international font rendering
- Register CanvasKit faces under their source family names so CJK and Arabic glyphs paint instead of only shaping metrics - Add deterministic CJK, Arabic, mixed-script, first-paint, and interactive fallback image oracles - Cover typed and tool-created text parity with a repository-owned Noto CJK subset Fixes #324 Fixes #291
This commit is contained in:
parent
47fe43c3ec
commit
9919d8710a
|
|
@ -36,6 +36,7 @@
|
||||||
- Make canvas text rendering demand missing font faces and verify CJK/Arabic fallback coverage from CanvasKit shaping results instead of coarse script predictions.
|
- Make canvas text rendering demand missing font faces and verify CJK/Arabic fallback coverage from CanvasKit shaping results instead of coarse script predictions.
|
||||||
- Resolve fonts before loaded, pasted, imported, and tool-created nodes render; invalidate generation-stale text caches and use baked `.fig` glyphs only after live font resolution is exhausted.
|
- Resolve fonts before loaded, pasted, imported, and tool-created nodes render; invalidate generation-stale text caches and use baked `.fig` glyphs only after live font resolution is exhausted.
|
||||||
- Load character-specific remote font subsets without Latin-only assumptions, preserve cumulative subset coverage, and reject unavailable desktop font styles instead of substituting the first family face.
|
- Load character-specific remote font subsets without Latin-only assumptions, preserve cumulative subset coverage, and reject unavailable desktop font styles instead of substituting the first family face.
|
||||||
|
- Render downloaded CJK and Arabic faces under their source family names so CanvasKit produces visible glyphs, with first-paint and interactive fallback snapshots covering CJK, Arabic, and mixed scripts.
|
||||||
- Fix live CLI and MCP automation drifting to the wrong open document or page when multiple files are open.
|
- Fix live CLI and MCP automation drifting to the wrong open document or page when multiple files are open.
|
||||||
- Improve Chinese, Japanese, and Korean text rendering with glyph-aware fallback fonts and outline rendering when needed.
|
- Improve Chinese, Japanese, and Korean text rendering with glyph-aware fallback fonts and outline rendering when needed.
|
||||||
- Preserve imported Figma text sizing more accurately, especially auto-sized text inside auto-layout frames.
|
- Preserve imported Figma text sizing more accurately, especially auto-sized text inside auto-layout frames.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
"ignoreWorkspaces": ["packages/acp", "packages/demos"],
|
"ignoreWorkspaces": ["packages/acp", "packages/demos"],
|
||||||
"ignoreUnresolved": [
|
"ignoreUnresolved": [
|
||||||
"~icons/*",
|
"~icons/*",
|
||||||
"/packages/core/src/text/fonts.ts",
|
|
||||||
"/src/app/editor/fonts/index.ts",
|
"/src/app/editor/fonts/index.ts",
|
||||||
"#core/*"
|
"#core/*"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import type { CanvasKit, TypefaceFontProvider } from 'canvaskit-wasm'
|
||||||
import type { SceneGraph } from '@open-pencil/scene-graph'
|
import type { SceneGraph } from '@open-pencil/scene-graph'
|
||||||
|
|
||||||
import { DEFAULT_FONT_FAMILY, IS_BROWSER } from '#core/constants'
|
import { DEFAULT_FONT_FAMILY, IS_BROWSER } from '#core/constants'
|
||||||
import { fontFaceRenderFamily } from '#core/text/face'
|
|
||||||
import {
|
import {
|
||||||
chooseLocalFontMatch,
|
chooseLocalFontMatch,
|
||||||
isVariableFont,
|
isVariableFont,
|
||||||
|
|
@ -52,9 +51,6 @@ export class FontManager {
|
||||||
private fallbackUserAgent: string | undefined
|
private fallbackUserAgent: string | undefined
|
||||||
private hostFontLoader: HostFontLoader | null = null
|
private hostFontLoader: HostFontLoader | null = null
|
||||||
private webFonts = new WebFontResolver()
|
private webFonts = new WebFontResolver()
|
||||||
private registeredRenderFamilies = new Set<string>()
|
|
||||||
private renderFamilyAliases = new Map<string, string>()
|
|
||||||
private renderFamilyRevisions = new Map<string, number>()
|
|
||||||
private cjkFallbackFamilies: string[] = []
|
private cjkFallbackFamilies: string[] = []
|
||||||
private cjkFallbackPromise: Promise<string[]> | null = null
|
private cjkFallbackPromise: Promise<string[]> | null = null
|
||||||
private arabicFallbackFamilies: string[] = []
|
private arabicFallbackFamilies: string[] = []
|
||||||
|
|
@ -64,18 +60,13 @@ export class FontManager {
|
||||||
this.fontProvider = provider
|
this.fontProvider = provider
|
||||||
this.registrationGeneration++
|
this.registrationGeneration++
|
||||||
this.providerRegistrations.clear()
|
this.providerRegistrations.clear()
|
||||||
this.registeredRenderFamilies.clear()
|
|
||||||
this.renderFamilyAliases.clear()
|
|
||||||
this.renderFamilyRevisions.clear()
|
|
||||||
for (const [cacheKey, data] of this.loadedFamilies) {
|
for (const [cacheKey, data] of this.loadedFamilies) {
|
||||||
const separator = cacheKey.indexOf('|')
|
const separator = cacheKey.indexOf('|')
|
||||||
const family = cacheKey.slice(0, separator)
|
const family = cacheKey.slice(0, separator)
|
||||||
const style = cacheKey.slice(separator + 1)
|
|
||||||
this.registerFontInCanvasKit(family, data)
|
this.registerFontInCanvasKit(family, data)
|
||||||
for (const supplemental of this.supplementalFamilyData.get(cacheKey) ?? []) {
|
for (const supplemental of this.supplementalFamilyData.get(cacheKey) ?? []) {
|
||||||
this.registerFontInCanvasKit(family, supplemental)
|
this.registerFontInCanvasKit(family, supplemental)
|
||||||
}
|
}
|
||||||
this.registerInitialRenderFamily(family, style, data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,23 +324,12 @@ export class FontManager {
|
||||||
return this.loadedFamilies.get(`${family}|${style}`) ?? null
|
return this.loadedFamilies.get(`${family}|${style}`) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFamily(family: string, style: string): string {
|
renderFamily(family: string, _style: string): string {
|
||||||
const data = this.loadedData(family, style)
|
// CanvasKit can shape metrics but paint no glyphs for some CJK/Arabic faces registered under a
|
||||||
if (!data) return family
|
// synthetic alias. Keep every shard under the font's source family; character-aware remote
|
||||||
|
// requests already fetch cumulative coverage before replacing the primary buffer.
|
||||||
const key = `${family}|${style}`
|
|
||||||
const renderFamily = this.renderFamilyAliases.get(key) ?? fontFaceRenderFamily(family, style)
|
|
||||||
if (!this.registeredRenderFamilies.has(renderFamily)) {
|
|
||||||
if (this.registerFontInCanvasKit(renderFamily, data)) {
|
|
||||||
this.renderFamilyAliases.set(key, renderFamily)
|
|
||||||
this.renderFamilyRevisions.set(key, 1)
|
|
||||||
this.registeredRenderFamilies.add(renderFamily)
|
|
||||||
} else {
|
|
||||||
return family
|
return family
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return renderFamily
|
|
||||||
}
|
|
||||||
|
|
||||||
collectFontKeys(graph: SceneGraph, nodeIds: string[]): Array<[string, string]> {
|
collectFontKeys(graph: SceneGraph, nodeIds: string[]): Array<[string, string]> {
|
||||||
return collectGraphFontKeys(graph, nodeIds)
|
return collectGraphFontKeys(graph, nodeIds)
|
||||||
|
|
@ -535,31 +515,10 @@ export class FontManager {
|
||||||
if (existing) this.registerSupplemental(family, style, existing)
|
if (existing) this.registerSupplemental(family, style, existing)
|
||||||
this.loadedFamilies.set(key, buffer)
|
this.loadedFamilies.set(key, buffer)
|
||||||
this.registerFontInCanvasKit(family, buffer)
|
this.registerFontInCanvasKit(family, buffer)
|
||||||
const currentRenderFamily = this.renderFamilyAliases.get(key)
|
|
||||||
if (currentRenderFamily) {
|
|
||||||
const revision = (this.renderFamilyRevisions.get(key) ?? 1) + 1
|
|
||||||
const renderFamily = `${fontFaceRenderFamily(family, style)}__${revision}`
|
|
||||||
if (this.registerFontInCanvasKit(renderFamily, buffer)) {
|
|
||||||
this.renderFamilyAliases.set(key, renderFamily)
|
|
||||||
this.renderFamilyRevisions.set(key, revision)
|
|
||||||
this.registeredRenderFamilies.add(renderFamily)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.registerInitialRenderFamily(family, style, buffer)
|
|
||||||
}
|
|
||||||
this.registerFontInBrowser(family, style, buffer)
|
this.registerFontInBrowser(family, style, buffer)
|
||||||
return buffer
|
return buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerInitialRenderFamily(family: string, style: string, data: ArrayBuffer): void {
|
|
||||||
const key = `${family}|${style}`
|
|
||||||
const renderFamily = fontFaceRenderFamily(family, style)
|
|
||||||
if (!this.registerFontInCanvasKit(renderFamily, data)) return
|
|
||||||
this.renderFamilyAliases.set(key, renderFamily)
|
|
||||||
this.renderFamilyRevisions.set(key, 1)
|
|
||||||
this.registeredRenderFamilies.add(renderFamily)
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerFontInCanvasKit(family: string, data: ArrayBuffer): boolean {
|
private registerFontInCanvasKit(family: string, data: ArrayBuffer): boolean {
|
||||||
if (!this.fontProvider || data.byteLength < 4) return false
|
if (!this.fontProvider || data.byteLength < 4) return false
|
||||||
const registeredData = this.providerRegistrations.get(family)
|
const registeredData = this.providerRegistrations.get(family)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
import type { FontManager } from '#core/text/fonts'
|
||||||
|
|
||||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||||
|
|
||||||
test('tool-created CJK text requests fallback through app font loading', async ({ page }) => {
|
test('tool-created CJK text requests fallback through app font loading', async ({ page }) => {
|
||||||
|
|
@ -13,13 +15,20 @@ test('tool-created CJK text requests fallback through app font loading', async (
|
||||||
|
|
||||||
const { ensureGraphFonts, loadFont } = await import('/src/app/editor/fonts/index.ts')
|
const { ensureGraphFonts, loadFont } = await import('/src/app/editor/fonts/index.ts')
|
||||||
await loadFont('Inter', 'Regular')
|
await loadFont('Inter', 'Regular')
|
||||||
const { fontManager } = await import('/packages/core/src/text/fonts.ts')
|
const fontModuleUrl = performance
|
||||||
|
.getEntriesByType('resource')
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.find((url) => url.includes('/packages/core/src/text/fonts.ts'))
|
||||||
|
if (!fontModuleUrl) throw new Error('Active font manager module not found')
|
||||||
|
const { fontManager } = (await import(/* @vite-ignore */ fontModuleUrl)) as {
|
||||||
|
fontManager: FontManager
|
||||||
|
}
|
||||||
const originalEnsureFallbackPack = fontManager.ensureFallbackPack.bind(fontManager)
|
const originalEnsureFallbackPack = fontManager.ensureFallbackPack.bind(fontManager)
|
||||||
let requestedScripts: string[] = []
|
let requestedScripts: string[] = []
|
||||||
|
|
||||||
fontManager.ensureFallbackPack = async (scripts = ['cjk', 'arabic']) => {
|
fontManager.ensureFallbackPack = async (scripts = ['cjk', 'arabic']) => {
|
||||||
requestedScripts = [...scripts]
|
requestedScripts = [...scripts]
|
||||||
return { cjk: ['Regression CJK Fallback'], arabic: [] }
|
return { cjk: ['Noto Sans CJK SC'], arabic: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageNode = store.graph.getNode(store.state.currentPageId)
|
const pageNode = store.graph.getNode(store.state.currentPageId)
|
||||||
|
|
@ -66,9 +75,16 @@ test('CJK text waits for fallback fonts and repaints after they load', async ({
|
||||||
const store = window.openPencil?.getStore?.()
|
const store = window.openPencil?.getStore?.()
|
||||||
if (!store?.renderer) throw new Error('OpenPencil renderer not initialized')
|
if (!store?.renderer) throw new Error('OpenPencil renderer not initialized')
|
||||||
const renderer = store.renderer
|
const renderer = store.renderer
|
||||||
const response = await fetch('/tests/fixtures/fonts/NotoSansSC-Regular.ttf')
|
const response = await fetch('/tests/fixtures/fonts/NotoSansCJK-Test.otf')
|
||||||
const fallbackData = await response.arrayBuffer()
|
const fallbackData = await response.arrayBuffer()
|
||||||
const { fontManager } = await import('/packages/core/src/text/fonts.ts')
|
const fontModuleUrl = performance
|
||||||
|
.getEntriesByType('resource')
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.find((url) => url.includes('/packages/core/src/text/fonts.ts'))
|
||||||
|
if (!fontModuleUrl) throw new Error('Active font manager module not found')
|
||||||
|
const { fontManager } = (await import(/* @vite-ignore */ fontModuleUrl)) as {
|
||||||
|
fontManager: FontManager
|
||||||
|
}
|
||||||
const manager = fontManager as typeof fontManager & { cjkFallbackFamilies: string[] }
|
const manager = fontManager as typeof fontManager & { cjkFallbackFamilies: string[] }
|
||||||
const originalFamilies = [...manager.cjkFallbackFamilies]
|
const originalFamilies = [...manager.cjkFallbackFamilies]
|
||||||
const originalEnsureFallbackPack = fontManager.ensureFallbackPack.bind(fontManager)
|
const originalEnsureFallbackPack = fontManager.ensureFallbackPack.bind(fontManager)
|
||||||
|
|
@ -84,9 +100,9 @@ test('CJK text waits for fallback fonts and repaints after they load', async ({
|
||||||
const originalRender = renderer.renderFromEditorState.bind(renderer)
|
const originalRender = renderer.renderFromEditorState.bind(renderer)
|
||||||
fontManager.ensureFallbackPack = async (scripts = ['cjk', 'arabic']) => {
|
fontManager.ensureFallbackPack = async (scripts = ['cjk', 'arabic']) => {
|
||||||
await fallbackGate
|
await fallbackGate
|
||||||
fontManager.markLoaded('Regression CJK Fallback', 'Regular', fallbackData)
|
fontManager.markLoaded('Noto Sans CJK SC', 'Regular', fallbackData)
|
||||||
fontManager.setCJKFallbackFamily('Regression CJK Fallback')
|
fontManager.setCJKFallbackFamily('Noto Sans CJK SC')
|
||||||
return Object.fromEntries(scripts.map((script) => [script, ['Regression CJK Fallback']]))
|
return Object.fromEntries(scripts.map((script) => [script, ['Noto Sans CJK SC']]))
|
||||||
}
|
}
|
||||||
renderer.renderFromEditorState = (
|
renderer.renderFromEditorState = (
|
||||||
...args: Parameters<typeof renderer.renderFromEditorState>
|
...args: Parameters<typeof renderer.renderFromEditorState>
|
||||||
|
|
@ -101,7 +117,7 @@ test('CJK text waits for fallback fonts and repaints after they load', async ({
|
||||||
y: 80,
|
y: 80,
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 60,
|
height: 60,
|
||||||
text: '上班打卡App',
|
text: '你好世界App',
|
||||||
textLanguage: 'zh-Hans',
|
textLanguage: 'zh-Hans',
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
|
|
@ -131,6 +147,7 @@ test('CJK text waits for fallback fonts and repaints after they load', async ({
|
||||||
return {
|
return {
|
||||||
loadedBeforeFallback,
|
loadedBeforeFallback,
|
||||||
loadedAfterFallback: renderer.isNodeFontLoaded(text),
|
loadedAfterFallback: renderer.isNodeFontLoaded(text),
|
||||||
|
readiness: renderer.nodeFontReadiness(text),
|
||||||
fallbackRenderCount,
|
fallbackRenderCount,
|
||||||
renderCount
|
renderCount
|
||||||
}
|
}
|
||||||
|
|
@ -142,6 +159,7 @@ test('CJK text waits for fallback fonts and repaints after they load', async ({
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.loadedBeforeFallback).toBe(false)
|
expect(result.loadedBeforeFallback).toBe(false)
|
||||||
|
expect(result.readiness).toBe('ready')
|
||||||
expect(result.loadedAfterFallback).toBe(true)
|
expect(result.loadedAfterFallback).toBe(true)
|
||||||
expect(result.fallbackRenderCount).toBe(1)
|
expect(result.fallbackRenderCount).toBe(1)
|
||||||
expect(result.renderCount).toBeGreaterThan(0)
|
expect(result.renderCount).toBeGreaterThan(0)
|
||||||
|
|
|
||||||
|
|
@ -1,121 +1,358 @@
|
||||||
import { existsSync, readFileSync } from 'node:fs'
|
import { Buffer } from 'node:buffer'
|
||||||
|
|
||||||
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
|
import type { FontManager } from '#core/text/fonts'
|
||||||
const appleGothic = '/System/Library/Fonts/Supplemental/AppleGothic.ttf'
|
|
||||||
|
|
||||||
test.skip(
|
import { expect, test } from '#tests/e2e/fixtures'
|
||||||
process.platform !== 'darwin' || !existsSync(appleGothic),
|
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||||
'CJK visual snapshot uses macOS AppleGothic for Hangul coverage'
|
|
||||||
)
|
|
||||||
|
|
||||||
async function expectCanvas(name: string) {
|
async function openEditor(page: Page): Promise<CanvasHelper> {
|
||||||
editor.canvas.assertNoErrors()
|
await page.goto('/?test&no-chrome&no-rulers')
|
||||||
const buffer = await editor.canvas.canvas.screenshot()
|
const canvas = new CanvasHelper(page)
|
||||||
expect(buffer).toMatchSnapshot(`${name}.png`)
|
await canvas.waitForInit()
|
||||||
}
|
await page.evaluate(() => {
|
||||||
|
|
||||||
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?.()
|
const store = window.openPencil?.getStore?.()
|
||||||
if (!store) throw new Error('OpenPencil store not initialized')
|
if (!store) throw new Error('OpenPencil store not initialized')
|
||||||
const pageId = store.state.currentPageId
|
const pageNode = store.graph.getNode(store.state.currentPageId)
|
||||||
const { fontManager } = await import('/packages/core/src/text/fonts.ts')
|
const childIds = pageNode?.childIds.slice() ?? []
|
||||||
|
for (const id of childIds) store.graph.deleteNode(id)
|
||||||
const [notoResponse, appleGothicResponse] = await Promise.all([
|
store.clearSelection()
|
||||||
fetch('/tests/fixtures/fonts/NotoSansSC-Regular.ttf'),
|
store.renderer?.invalidateAllPictures()
|
||||||
fetch('/__test-fonts/apple-gothic.ttf')
|
store.requestRender()
|
||||||
])
|
store.renderer?.renderFromEditorState(
|
||||||
if (!notoResponse.ok) throw new Error(`Failed to load Noto Sans SC: ${notoResponse.status}`)
|
store.state,
|
||||||
if (!appleGothicResponse.ok) {
|
store.graph,
|
||||||
throw new Error(`Failed to load AppleGothic: ${appleGothicResponse.status}`)
|
store.textEditor,
|
||||||
}
|
window.innerWidth,
|
||||||
|
window.innerHeight,
|
||||||
fontManager.markLoaded('Noto Sans SC', 'Regular', await notoResponse.arrayBuffer())
|
false,
|
||||||
fontManager.markLoaded('AppleGothic', 'Regular', await appleGothicResponse.arrayBuffer())
|
'full'
|
||||||
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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
|
await canvas.waitForRender()
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
|
||||||
for (const [index, line] of lines.entries()) {
|
async function expectCanvas(canvas: CanvasHelper, name: string): Promise<void> {
|
||||||
const y = 84 + index * 64
|
await canvas.page.evaluate(
|
||||||
|
() =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(resolve))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
canvas.assertNoErrors()
|
||||||
|
expect(await canvas.canvas.screenshot()).toMatchSnapshot(`${name}.png`)
|
||||||
|
}
|
||||||
|
|
||||||
|
test('international text is correct on its first visible paint', async ({ page }) => {
|
||||||
|
await openEditor(page)
|
||||||
|
|
||||||
|
const result = await page.evaluate(async () => {
|
||||||
|
const store = window.openPencil?.getStore?.()
|
||||||
|
if (!store?.renderer) throw new Error('OpenPencil renderer not initialized')
|
||||||
|
const fontModuleUrl = performance
|
||||||
|
.getEntriesByType('resource')
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.find((url) => url.includes('/packages/core/src/text/fonts.ts'))
|
||||||
|
if (!fontModuleUrl) throw new Error('Active font manager module not found')
|
||||||
|
const { fontManager } = (await import(/* @vite-ignore */ fontModuleUrl)) as {
|
||||||
|
fontManager: FontManager
|
||||||
|
}
|
||||||
|
const pageId = store.state.currentPageId
|
||||||
|
const samples = [
|
||||||
|
{
|
||||||
|
label: 'Simplified Chinese',
|
||||||
|
text: '你好世界',
|
||||||
|
language: 'zh-Hans',
|
||||||
|
family: 'Noto Sans CJK SC'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Traditional Chinese',
|
||||||
|
text: '繁體中文',
|
||||||
|
language: 'zh-Hant',
|
||||||
|
family: 'Noto Sans CJK SC'
|
||||||
|
},
|
||||||
|
{ label: 'Japanese', text: '日本語かなカナ', language: 'ja', family: 'Noto Sans CJK SC' },
|
||||||
|
{ label: 'Korean', text: '안녕하세요', language: 'ko', family: 'Noto Sans CJK SC' },
|
||||||
|
{ label: 'Arabic', text: 'مرحبا بالعالم', language: 'ar', family: 'Noto Naskh Arabic' },
|
||||||
|
{
|
||||||
|
label: 'Mixed scripts',
|
||||||
|
text: 'OpenPencil · 你好 · مرحبا',
|
||||||
|
language: 'en',
|
||||||
|
family: 'Noto Sans CJK SC'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const sampleIds = samples.map((sample, index) => {
|
||||||
|
const y = 70 + index * 62
|
||||||
store.graph.createNode('TEXT', pageId, {
|
store.graph.createNode('TEXT', pageId, {
|
||||||
name: `${line.label} label`,
|
name: `${sample.label} label`,
|
||||||
x: 96,
|
x: 88,
|
||||||
y,
|
y: y + 4,
|
||||||
width: 210,
|
width: 220,
|
||||||
height: 28,
|
height: 28,
|
||||||
text: line.label,
|
text: sample.label,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
fontSize: 17,
|
fontSize: 16,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fills: [
|
fills: [
|
||||||
{ type: 'SOLID', color: { r: 0.25, g: 0.32, b: 0.45, a: 1 }, visible: true, opacity: 1 }
|
{ type: 'SOLID', color: { r: 0.25, g: 0.32, b: 0.45, a: 1 }, visible: true, opacity: 1 }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
store.graph.createNode('TEXT', pageId, {
|
return store.graph.createNode('TEXT', pageId, {
|
||||||
name: `${line.label} sample`,
|
name: `${sample.label} sample`,
|
||||||
x: 328,
|
x: 330,
|
||||||
y: y - 4,
|
y,
|
||||||
width: 360,
|
width: 430,
|
||||||
height: 42,
|
height: 44,
|
||||||
text: line.text,
|
text: sample.text,
|
||||||
fontFamily: 'Inter',
|
textLanguage: sample.language,
|
||||||
fontSize: 32,
|
fontFamily: sample.family,
|
||||||
textAutoResize: 'HEIGHT',
|
fontSize: 30,
|
||||||
|
textAutoResize: 'NONE',
|
||||||
|
styleRuns:
|
||||||
|
sample.label === 'Mixed scripts'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
start: sample.text.indexOf('مرحبا'),
|
||||||
|
length: 'مرحبا'.length,
|
||||||
|
style: { fontFamily: 'Noto Naskh Arabic', textLanguage: 'ar' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [],
|
||||||
fills: [
|
fills: [
|
||||||
{ type: 'SOLID', color: { r: 0.04, g: 0.06, b: 0.12, a: 1 }, visible: true, opacity: 1 }
|
{ type: 'SOLID', color: { r: 0.04, g: 0.06, b: 0.12, a: 1 }, visible: true, opacity: 1 }
|
||||||
]
|
]
|
||||||
|
}).id
|
||||||
|
})
|
||||||
|
|
||||||
|
fontManager.blockNodesUntilFontsResolve(sampleIds)
|
||||||
|
const blockedBeforeFonts = sampleIds.every((id) => fontManager.isNodeBlocked(id))
|
||||||
|
const [cjk, arabic] = await Promise.all([
|
||||||
|
fetch('/tests/fixtures/fonts/NotoSansCJK-Test.otf').then((response) =>
|
||||||
|
response.arrayBuffer()
|
||||||
|
),
|
||||||
|
fetch('/tests/fixtures/fonts/NotoNaskhArabic-Regular.ttf').then((response) =>
|
||||||
|
response.arrayBuffer()
|
||||||
|
)
|
||||||
|
])
|
||||||
|
fontManager.markLoaded('Noto Sans CJK SC', 'Regular', cjk)
|
||||||
|
fontManager.markLoaded('Noto Naskh Arabic', 'Regular', arabic)
|
||||||
|
fontManager.setCJKFallbackFamily('Noto Sans CJK SC')
|
||||||
|
fontManager.setArabicFallbackFamily('Noto Naskh Arabic')
|
||||||
|
|
||||||
|
await store.loadFontsForNodes(sampleIds)
|
||||||
|
fontManager.unblockNodes(sampleIds)
|
||||||
|
for (const id of sampleIds) {
|
||||||
|
const node = store.graph.getNode(id)
|
||||||
|
if (node?.type === 'TEXT') store.graph.updateNode(id, { text: node.text })
|
||||||
|
}
|
||||||
|
store.clearSelection()
|
||||||
|
store.renderer.invalidateAllPictures()
|
||||||
|
store.requestRender()
|
||||||
|
store.renderer.renderFromEditorState(
|
||||||
|
store.state,
|
||||||
|
store.graph,
|
||||||
|
store.textEditor,
|
||||||
|
window.innerWidth,
|
||||||
|
window.innerHeight,
|
||||||
|
false,
|
||||||
|
'full'
|
||||||
|
)
|
||||||
|
const pageNode = store.graph.getNode(pageId)
|
||||||
|
const image = await store.renderExportImage(pageNode?.childIds ?? [], 2, 'PNG')
|
||||||
|
if (!image) throw new Error('First-paint export failed')
|
||||||
|
return {
|
||||||
|
blockedBeforeFonts,
|
||||||
|
image: Array.from(image),
|
||||||
|
readiness: sampleIds.map((id) => {
|
||||||
|
const node = store.graph.getNode(id)
|
||||||
|
return {
|
||||||
|
name: node?.name,
|
||||||
|
state: node ? store.renderer?.nodeFontReadiness(node) : 'missing'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
unblockedAfterFonts: sampleIds.every((id) => !fontManager.isNodeBlocked(id))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { image, ...state } = result
|
||||||
|
expect(state).toEqual({
|
||||||
|
blockedBeforeFonts: true,
|
||||||
|
readiness: [
|
||||||
|
{ name: 'Simplified Chinese sample', state: 'ready' },
|
||||||
|
{ name: 'Traditional Chinese sample', state: 'ready' },
|
||||||
|
{ name: 'Japanese sample', state: 'ready' },
|
||||||
|
{ name: 'Korean sample', state: 'ready' },
|
||||||
|
{ name: 'Arabic sample', state: 'ready' },
|
||||||
|
{ name: 'Mixed scripts sample', state: 'ready' }
|
||||||
|
],
|
||||||
|
unblockedAfterFonts: true
|
||||||
|
})
|
||||||
|
expect(Buffer.from(image)).toMatchSnapshot('international-text-first-paint.png')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('typed and tool-created text repaint with the same resolved fallbacks', async ({ page }) => {
|
||||||
|
const canvas = await openEditor(page)
|
||||||
|
|
||||||
|
const ids = await page.evaluate(async () => {
|
||||||
|
const store = window.openPencil?.getStore?.()
|
||||||
|
if (!store) throw new Error('OpenPencil store not initialized')
|
||||||
|
const pageId = store.state.currentPageId
|
||||||
|
|
||||||
|
const labels = ['Tool-created', 'Interactively typed']
|
||||||
|
for (const [index, label] of labels.entries()) {
|
||||||
|
store.graph.createNode('TEXT', pageId, {
|
||||||
|
name: `${label} label`,
|
||||||
|
x: 88,
|
||||||
|
y: 112 + index * 92,
|
||||||
|
width: 190,
|
||||||
|
height: 28,
|
||||||
|
text: label,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
fills: [
|
||||||
|
{ type: 'SOLID', color: { r: 0.25, g: 0.32, b: 0.45, a: 1 }, visible: true, opacity: 1 }
|
||||||
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await store.loadFontsForNodes(store.graph.getPages().flatMap((page) => page.childIds))
|
const common = {
|
||||||
store.clearSelection()
|
width: 450,
|
||||||
store.requestRender()
|
height: 52,
|
||||||
|
textLanguage: 'zh-Hans',
|
||||||
return {
|
fontFamily: 'Inter',
|
||||||
loaded: {
|
fontSize: 30,
|
||||||
noto: fontManager.isStyleLoaded('Noto Sans SC', 'Regular'),
|
textAutoResize: 'NONE' as const,
|
||||||
appleGothic: fontManager.isStyleLoaded('AppleGothic', 'Regular')
|
fills: [
|
||||||
},
|
{
|
||||||
fallbacks: fontManager.getCJKFallbackFamilies()
|
type: 'SOLID' as const,
|
||||||
|
color: { r: 0.04, g: 0.06, b: 0.12, a: 1 },
|
||||||
|
visible: true,
|
||||||
|
opacity: 1
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const tool = store.graph.createNode('TEXT', pageId, {
|
||||||
|
...common,
|
||||||
|
name: 'Tool-created mixed script',
|
||||||
|
x: 300,
|
||||||
|
y: 104,
|
||||||
|
text: 'OpenPencil · 你好 · مرحبا'
|
||||||
|
})
|
||||||
|
const typed = store.graph.createNode('TEXT', pageId, {
|
||||||
|
...common,
|
||||||
|
name: 'Interactively typed mixed script',
|
||||||
|
x: 300,
|
||||||
|
y: 196,
|
||||||
|
text: ''
|
||||||
|
})
|
||||||
|
const nodeIds = [tool.id, typed.id]
|
||||||
|
const fontModuleUrl = performance
|
||||||
|
.getEntriesByType('resource')
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.find((url) => url.includes('/packages/core/src/text/fonts.ts'))
|
||||||
|
if (!fontModuleUrl) throw new Error('Active font manager module not found')
|
||||||
|
const { fontManager } = (await import(/* @vite-ignore */ fontModuleUrl)) as {
|
||||||
|
fontManager: FontManager
|
||||||
|
}
|
||||||
|
fontManager.blockNodesUntilFontsResolve(nodeIds)
|
||||||
|
store.select([typed.id])
|
||||||
|
store.startTextEditing(typed.id)
|
||||||
|
store.requestRender()
|
||||||
|
return { nodeIds, toolId: tool.id, typedId: typed.id }
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(smoke.loaded).toEqual({ noto: true, appleGothic: true })
|
await page.locator('textarea[aria-hidden="true"]').fill('OpenPencil · 你好 · مرحبا')
|
||||||
expect(smoke.fallbacks).toEqual(expect.arrayContaining(['Noto Sans SC', 'AppleGothic']))
|
await page.evaluate(() => {
|
||||||
await editor.canvas.waitForRender()
|
const store = window.openPencil?.getStore?.()
|
||||||
await expectCanvas('cjk-fallback-text-rendering')
|
if (!store?.renderer) throw new Error('OpenPencil renderer not initialized')
|
||||||
|
store.renderer.invalidateAllPictures()
|
||||||
|
store.renderer.renderFromEditorState(
|
||||||
|
store.state,
|
||||||
|
store.graph,
|
||||||
|
store.textEditor,
|
||||||
|
window.innerWidth,
|
||||||
|
window.innerHeight,
|
||||||
|
false,
|
||||||
|
'full'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await canvas.waitForRender()
|
||||||
|
const pending = await page.evaluate(async (nodeIds) => {
|
||||||
|
const fontModuleUrl = performance
|
||||||
|
.getEntriesByType('resource')
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.find((url) => url.includes('/packages/core/src/text/fonts.ts'))
|
||||||
|
if (!fontModuleUrl) throw new Error('Active font manager module not found')
|
||||||
|
const { fontManager } = (await import(/* @vite-ignore */ fontModuleUrl)) as {
|
||||||
|
fontManager: FontManager
|
||||||
|
}
|
||||||
|
return nodeIds.every((id) => fontManager.isNodeBlocked(id))
|
||||||
|
}, ids.nodeIds)
|
||||||
|
expect(pending).toBe(true)
|
||||||
|
await expectCanvas(canvas, 'interactive-font-fallback-pending')
|
||||||
|
|
||||||
|
const resolved = await page.evaluate(async ({ nodeIds, toolId, typedId }) => {
|
||||||
|
const store = window.openPencil?.getStore?.()
|
||||||
|
if (!store?.renderer) throw new Error('OpenPencil renderer not initialized')
|
||||||
|
const fontModuleUrl = performance
|
||||||
|
.getEntriesByType('resource')
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.find((url) => url.includes('/packages/core/src/text/fonts.ts'))
|
||||||
|
if (!fontModuleUrl) throw new Error('Active font manager module not found')
|
||||||
|
const { fontManager } = (await import(/* @vite-ignore */ fontModuleUrl)) as {
|
||||||
|
fontManager: FontManager
|
||||||
|
}
|
||||||
|
const [cjk, arabic] = await Promise.all([
|
||||||
|
fetch('/tests/fixtures/fonts/NotoSansCJK-Test.otf').then((response) =>
|
||||||
|
response.arrayBuffer()
|
||||||
|
),
|
||||||
|
fetch('/tests/fixtures/fonts/NotoNaskhArabic-Regular.ttf').then((response) =>
|
||||||
|
response.arrayBuffer()
|
||||||
|
)
|
||||||
|
])
|
||||||
|
fontManager.markLoaded('Noto Sans CJK SC', 'Regular', cjk)
|
||||||
|
fontManager.markLoaded('Noto Naskh Arabic', 'Regular', arabic)
|
||||||
|
fontManager.setCJKFallbackFamily('Noto Sans CJK SC')
|
||||||
|
fontManager.setArabicFallbackFamily('Noto Naskh Arabic')
|
||||||
|
store.commitTextEdit()
|
||||||
|
await store.loadFontsForNodes(nodeIds)
|
||||||
|
fontManager.unblockNodes(nodeIds)
|
||||||
|
for (const id of nodeIds) {
|
||||||
|
const node = store.graph.getNode(id)
|
||||||
|
if (node?.type === 'TEXT') store.graph.updateNode(id, { text: node.text })
|
||||||
|
}
|
||||||
|
store.clearSelection()
|
||||||
|
store.renderer.invalidateAllPictures()
|
||||||
|
store.requestRender()
|
||||||
|
store.renderer.renderFromEditorState(
|
||||||
|
store.state,
|
||||||
|
store.graph,
|
||||||
|
store.textEditor,
|
||||||
|
window.innerWidth,
|
||||||
|
window.innerHeight,
|
||||||
|
false,
|
||||||
|
'full'
|
||||||
|
)
|
||||||
|
|
||||||
|
const tool = store.graph.getNode(toolId)
|
||||||
|
const typed = store.graph.getNode(typedId)
|
||||||
|
const pageNode = store.graph.getNode(store.state.currentPageId)
|
||||||
|
const image = await store.renderExportImage(pageNode?.childIds ?? [], 2, 'PNG')
|
||||||
|
if (!image) throw new Error('Interactive export failed')
|
||||||
|
return {
|
||||||
|
image: Array.from(image),
|
||||||
|
textsMatch: tool?.type === 'TEXT' && typed?.type === 'TEXT' && tool.text === typed.text,
|
||||||
|
toolReady: tool ? store.renderer.isNodeFontLoaded(tool) : false,
|
||||||
|
typedReady: typed ? store.renderer.isNodeFontLoaded(typed) : false,
|
||||||
|
unblocked: nodeIds.every((id) => !fontManager.isNodeBlocked(id))
|
||||||
|
}
|
||||||
|
}, ids)
|
||||||
|
|
||||||
|
const { image, ...state } = resolved
|
||||||
|
expect(state).toEqual({ textsMatch: true, toolReady: true, typedReady: true, unblocked: true })
|
||||||
|
expect(Buffer.from(image)).toMatchSnapshot('interactive-font-fallback-resolved.png')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
|
|
@ -25,10 +25,10 @@ describe('font lifecycle', () => {
|
||||||
expect(providerGeneration).toBeGreaterThan(0)
|
expect(providerGeneration).toBeGreaterThan(0)
|
||||||
expect(registrationGeneration).toBeGreaterThan(providerGeneration)
|
expect(registrationGeneration).toBeGreaterThan(providerGeneration)
|
||||||
expect(manager.generation()).toBe(registrationGeneration)
|
expect(manager.generation()).toBe(registrationGeneration)
|
||||||
expect(registrations).toEqual(['Generation Test', '__op_font__Generation_Test__Regular'])
|
expect(registrations).toEqual(['Generation Test'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('moves replaced subset fonts to a fresh render family', () => {
|
test('keeps cumulative subset registrations under the source family', () => {
|
||||||
const manager = new FontManager()
|
const manager = new FontManager()
|
||||||
const registrations: string[] = []
|
const registrations: string[] = []
|
||||||
const provider = {
|
const provider = {
|
||||||
|
|
@ -39,13 +39,12 @@ describe('font lifecycle', () => {
|
||||||
|
|
||||||
manager.attachProvider({} as CanvasKit, provider)
|
manager.attachProvider({} as CanvasKit, provider)
|
||||||
manager.markLoaded('Subset Font', 'Regular', new ArrayBuffer(8))
|
manager.markLoaded('Subset Font', 'Regular', new ArrayBuffer(8))
|
||||||
const firstRenderFamily = manager.renderFamily('Subset Font', 'Regular')
|
const firstGeneration = manager.generation()
|
||||||
manager.markLoaded('Subset Font', 'Regular', new ArrayBuffer(12))
|
manager.markLoaded('Subset Font', 'Regular', new ArrayBuffer(12))
|
||||||
const secondRenderFamily = manager.renderFamily('Subset Font', 'Regular')
|
|
||||||
|
|
||||||
expect(firstRenderFamily).toBe('__op_font__Subset_Font__Regular')
|
expect(manager.renderFamily('Subset Font', 'Regular')).toBe('Subset Font')
|
||||||
expect(secondRenderFamily).toBe('__op_font__Subset_Font__Regular__2')
|
expect(manager.generation()).toBeGreaterThan(firstGeneration)
|
||||||
expect(registrations).toContain(secondRenderFamily)
|
expect(registrations).toEqual(['Subset Font', 'Subset Font'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('tracks nodes gated by pre-render font resolution', () => {
|
test('tracks nodes gated by pre-render font resolution', () => {
|
||||||
|
|
|
||||||
|
|
@ -132,16 +132,10 @@ describe('FontManager loaded font cache', () => {
|
||||||
|
|
||||||
manager.attachProvider(canvasKit, first.provider)
|
manager.attachProvider(canvasKit, first.provider)
|
||||||
manager.markLoaded('ProviderLifecycle', 'Regular', new ArrayBuffer(12))
|
manager.markLoaded('ProviderLifecycle', 'Regular', new ArrayBuffer(12))
|
||||||
expect(first.registrations).toEqual([
|
expect(first.registrations).toEqual([{ family: 'ProviderLifecycle', byteLength: 12 }])
|
||||||
{ family: 'ProviderLifecycle', byteLength: 12 },
|
|
||||||
{ family: '__op_font__ProviderLifecycle__Regular', byteLength: 12 }
|
|
||||||
])
|
|
||||||
|
|
||||||
manager.attachProvider(canvasKit, second.provider)
|
manager.attachProvider(canvasKit, second.provider)
|
||||||
expect(second.registrations).toEqual([
|
expect(second.registrations).toEqual([{ family: 'ProviderLifecycle', byteLength: 12 }])
|
||||||
{ family: 'ProviderLifecycle', byteLength: 12 },
|
|
||||||
{ family: '__op_font__ProviderLifecycle__Regular', byteLength: 12 }
|
|
||||||
])
|
|
||||||
manager.detachProvider(first.provider)
|
manager.detachProvider(first.provider)
|
||||||
expect(manager.provider()).toBe(second.provider)
|
expect(manager.provider()).toBe(second.provider)
|
||||||
|
|
||||||
|
|
@ -149,7 +143,7 @@ describe('FontManager loaded font cache', () => {
|
||||||
expect(manager.provider()).toBeNull()
|
expect(manager.provider()).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('registers loaded faces under exact render families', () => {
|
test('renders loaded faces under their source families', () => {
|
||||||
const manager = new FontManager()
|
const manager = new FontManager()
|
||||||
const recording = createRecordingProvider()
|
const recording = createRecordingProvider()
|
||||||
|
|
||||||
|
|
@ -158,13 +152,10 @@ describe('FontManager loaded font cache', () => {
|
||||||
|
|
||||||
const renderFamily = manager.renderFamily('Inter', 'SemiBold')
|
const renderFamily = manager.renderFamily('Inter', 'SemiBold')
|
||||||
|
|
||||||
expect(renderFamily).toBe('__op_font__Inter__SemiBold')
|
expect(renderFamily).toBe('Inter')
|
||||||
expect(recording.registrations).toEqual([
|
expect(recording.registrations).toEqual([{ family: 'Inter', byteLength: 12 }])
|
||||||
{ family: 'Inter', byteLength: 12 },
|
|
||||||
{ family: '__op_font__Inter__SemiBold', byteLength: 12 }
|
|
||||||
])
|
|
||||||
expect(manager.renderFamily('Inter', 'SemiBold')).toBe(renderFamily)
|
expect(manager.renderFamily('Inter', 'SemiBold')).toBe(renderFamily)
|
||||||
expect(recording.registrations).toHaveLength(2)
|
expect(recording.registrations).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('prefers local font data before downloaded cache', async () => {
|
test('prefers local font data before downloaded cache', async () => {
|
||||||
|
|
@ -206,10 +197,7 @@ describe('FontManager loaded font cache', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(manager.loadFont('DownloadedCache', 'Regular')).resolves.toBe(data)
|
await expect(manager.loadFont('DownloadedCache', 'Regular')).resolves.toBe(data)
|
||||||
expect(recording.registrations).toEqual([
|
expect(recording.registrations).toEqual([{ family: 'DownloadedCache', byteLength: 16 }])
|
||||||
{ family: 'DownloadedCache', byteLength: 16 },
|
|
||||||
{ family: '__op_font__DownloadedCache__Regular', byteLength: 16 }
|
|
||||||
])
|
|
||||||
expect(writes).toBe(0)
|
expect(writes).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -226,10 +214,7 @@ describe('FontManager loaded font cache', () => {
|
||||||
const data = await manager.loadFont('Inter', 'ExtraBold')
|
const data = await manager.loadFont('Inter', 'ExtraBold')
|
||||||
|
|
||||||
expect(data?.byteLength).toBeGreaterThan(0)
|
expect(data?.byteLength).toBeGreaterThan(0)
|
||||||
expect(recording.registrations).toEqual([
|
expect(recording.registrations).toEqual([{ family: 'Inter', byteLength: data?.byteLength }])
|
||||||
{ family: 'Inter', byteLength: data?.byteLength },
|
|
||||||
{ family: '__op_font__Inter__ExtraBold', byteLength: data?.byteLength }
|
|
||||||
])
|
|
||||||
} finally {
|
} finally {
|
||||||
globalThis.fetch = originalFetch
|
globalThis.fetch = originalFetch
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
tests/fixtures/fonts/NotoSansCJK-Test.otf
vendored
Normal file
BIN
tests/fixtures/fonts/NotoSansCJK-Test.otf
vendored
Normal file
Binary file not shown.
93
tests/fixtures/fonts/OFL.txt
vendored
Normal file
93
tests/fixtures/fonts/OFL.txt
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Loading…
Reference in a new issue