openpencil/tests/engine/render/canvas/text-font-variations.test.ts
Danila Poyarkov a2fc235131
fix(text): render variable font styles at their named instances (#773)
* fix(text): render variable font styles at their named instances

Installed variable fonts such as SF Pro list every named instance, but
font-kit loads each one at the default weight, so the desktop loader
rejected Medium and Bold and the canvas fell back to a substitute. Even
when a variable face was loaded, CanvasKit drew it at its default axes:
Medium rendered as Regular and Bold as a synthetic bold.

The desktop loader now falls back to a variable face whose wght axis
covers the requested weight. The renderer applies the coordinates of the
named instance matching the style, or the clamped weight when none
matches, beneath any explicit font variations on the text.

* fix(text): validate variable font tables and leave loading to the host

Check name-table records and string ranges, the fvar header size, and
axis and instance record sizes, so a malformed font falls back to the
style weight instead of throwing while text is shaped.

Drop the desktop loader's variable-face fallback; system font discovery
moves to fontique, which lists variable faces with their weight axes.
2026-09-26 11:25:01 +04:00

86 lines
2.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import {
textDecorationStyleValue,
textFontFeatures,
textFontVariations,
textHeightBehaviorValue
} from '#core/canvas/text'
describe('canvas text font variations', () => {
test('maps imported text decoration styles to CanvasKit', () => {
const ck = {
DecorationStyle: {
Solid: 'solid',
Dotted: 'dotted',
Wavy: 'wavy'
}
}
expect(textDecorationStyleValue(ck, 'SOLID')).toBe('solid')
expect(textDecorationStyleValue(ck, 'DOTTED')).toBe('dotted')
expect(textDecorationStyleValue(ck, 'WAVY')).toBe('wavy')
expect(textDecorationStyleValue(ck, undefined)).toBe('solid')
})
test('maps imported leading trim to CanvasKit text height behavior', () => {
const ck = {
TextHeightBehavior: {
DisableAll: 'disable-all'
}
}
expect(textHeightBehaviorValue(ck, 'CAP_HEIGHT')).toBe('disable-all')
expect(textHeightBehaviorValue(ck, 'NONE')).toBeUndefined()
})
test('passes imported variable font axes to CanvasKit text styles', () => {
expect(
textFontVariations([
{ axis: 'wght', value: 650 },
{ axis: 'wdth', value: 88 }
])
).toEqual([
{ axis: 'wght', value: 650 },
{ axis: 'wdth', value: 88 }
])
})
test('omits font variations when no axes are set', () => {
expect(textFontVariations([])).toBeUndefined()
expect(textFontVariations(undefined)).toBeUndefined()
expect(textFontVariations(undefined, [])).toBeUndefined()
})
test('applies named-instance axes of a variable face under explicit axes', () => {
const medium = [
{ axis: 'wdth', value: 100 },
{ axis: 'wght', value: 510 }
]
expect(textFontVariations(undefined, medium)).toEqual(medium)
expect(textFontVariations([{ axis: 'wght', value: 650 }], medium)).toEqual([
{ axis: 'wdth', value: 100 },
{ axis: 'wght', value: 650 }
])
})
test('passes imported OpenType feature toggles to CanvasKit text styles', () => {
expect(
textFontFeatures([
{ tag: 'LIGA', enabled: false },
{ tag: 'CALT', enabled: true },
{ tag: 'SS01', enabled: true }
])
).toEqual([
{ name: 'liga', value: 0 },
{ name: 'calt', value: 1 },
{ name: 'ss01', value: 1 }
])
})
test('omits font features when no toggles are set', () => {
expect(textFontFeatures([])).toBeUndefined()
expect(textFontFeatures(undefined)).toBeUndefined()
})
})