fix: normalize font family names on .fig export
Strip optical size suffixes (e.g. "DM Sans 9pt" → "DM Sans") and "Variable" suffixes when writing fontName.family to .fig files. This ensures Figma recognizes the font instead of showing a "Missing font" dialog. Closes #131
This commit is contained in:
parent
8599750f1d
commit
2efd63ea09
|
|
@ -16,6 +16,7 @@
|
|||
- Fix imported text rendering in browser and headless export — preserve stored bounds until fonts are ready, restore missing font-loaded guards, use natural width for `WIDTH_AND_HEIGHT` text, and clip text to node bounds
|
||||
- Fix browser/headless rendering mismatch for imported toolbar/instance content by correcting runtime imported layout recomputation instead of diverging browser rendering behavior
|
||||
- Fix `set_layout` tool not defaulting to HUG sizing when enabling auto-layout — frames now shrink/grow to fit children instead of keeping fixed dimensions
|
||||
- Normalize font family names on `.fig` export — strip optical size suffixes (e.g. "DM Sans 9pt" → "DM Sans") so Figma recognizes the font
|
||||
- Fix save crash when COLOR variable is missing alpha channel
|
||||
- Fix console error spam on deployed web app from automation WebSocket reconnect loop
|
||||
- Fix headless CLI font fallback — bundled Inter font now ships with `@open-pencil/core` and loads without a web server
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ const googleFontsCache = new Map<string, Record<string, string>>()
|
|||
const googleFontsFailed = new Set<string>()
|
||||
|
||||
export function normalizeFontFamily(family: string): string {
|
||||
return family.replace(/\s+Variable$/i, '')
|
||||
return family.replace(/\s+(Variable|\d+(?:pt|px|em))$/i, '')
|
||||
}
|
||||
|
||||
async function retryWithNormalizedFamily(family: string): Promise<Record<string, string> | null> {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export const FIG_KIWI_VERSION = 106
|
|||
|
||||
import { deflateSync, inflateSync } from 'fflate'
|
||||
|
||||
import { weightToStyle, getLoadedFontData } from '../fonts'
|
||||
import { getLoadedFontData, normalizeFontFamily, weightToStyle } from '../fonts'
|
||||
import { encodeVectorNetworkBlob } from '../vector'
|
||||
import { stringToGuid, VARIABLE_BINDING_FIELDS } from './kiwi-convert'
|
||||
|
||||
|
|
@ -225,7 +225,7 @@ function exportTextData(node: SceneNode): NodeChange['textData'] {
|
|||
const weight = style.fontWeight ?? node.fontWeight
|
||||
const italic = style.italic ?? node.italic
|
||||
override.fontName = {
|
||||
family: style.fontFamily ?? node.fontFamily,
|
||||
family: normalizeFontFamily(style.fontFamily ?? node.fontFamily),
|
||||
style: weightToStyle(weight, italic),
|
||||
postscript: ''
|
||||
}
|
||||
|
|
@ -302,7 +302,7 @@ function serializeTextProps(
|
|||
): void {
|
||||
nc.fontSize = node.fontSize
|
||||
nc.fontName = {
|
||||
family: node.fontFamily,
|
||||
family: normalizeFontFamily(node.fontFamily),
|
||||
style: weightToStyle(node.fontWeight, node.italic),
|
||||
postscript: ''
|
||||
}
|
||||
|
|
|
|||
84
tests/engine/font-normalize-export.test.ts
Normal file
84
tests/engine/font-normalize-export.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { describe, test, expect, beforeAll } from 'bun:test'
|
||||
|
||||
import {
|
||||
exportFigFile,
|
||||
parseFigFile,
|
||||
initCodec,
|
||||
SceneGraph,
|
||||
} from '@open-pencil/core'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCodec()
|
||||
})
|
||||
|
||||
function pageId(graph: SceneGraph) {
|
||||
return graph.getPages()[0].id
|
||||
}
|
||||
|
||||
describe('Font family normalization on .fig export', () => {
|
||||
test('strips optical size suffix from font family', async () => {
|
||||
const graph = new SceneGraph()
|
||||
graph.createNode('TEXT', pageId(graph), {
|
||||
name: 'Test',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 20,
|
||||
text: 'Hello',
|
||||
fontFamily: 'DM Sans 9pt',
|
||||
fontWeight: 400,
|
||||
fontSize: 14,
|
||||
})
|
||||
|
||||
const exported = await exportFigFile(graph)
|
||||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
|
||||
const nodes = [...reimported.nodes.values()]
|
||||
const textNode = nodes.find((n) => n.type === 'TEXT')!
|
||||
expect(textNode.fontFamily).toBe('DM Sans')
|
||||
})
|
||||
|
||||
test('preserves normal font family names', async () => {
|
||||
const graph = new SceneGraph()
|
||||
graph.createNode('TEXT', pageId(graph), {
|
||||
name: 'Test',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 20,
|
||||
text: 'Hello',
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: 400,
|
||||
fontSize: 14,
|
||||
})
|
||||
|
||||
const exported = await exportFigFile(graph)
|
||||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
|
||||
const nodes = [...reimported.nodes.values()]
|
||||
const textNode = nodes.find((n) => n.type === 'TEXT')!
|
||||
expect(textNode.fontFamily).toBe('Inter')
|
||||
})
|
||||
|
||||
test('strips Variable suffix from font family', async () => {
|
||||
const graph = new SceneGraph()
|
||||
graph.createNode('TEXT', pageId(graph), {
|
||||
name: 'Test',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 20,
|
||||
text: 'Hello',
|
||||
fontFamily: 'Roboto Variable',
|
||||
fontWeight: 400,
|
||||
fontSize: 14,
|
||||
})
|
||||
|
||||
const exported = await exportFigFile(graph)
|
||||
const reimported = await parseFigFile(exported.buffer as ArrayBuffer)
|
||||
|
||||
const nodes = [...reimported.nodes.values()]
|
||||
const textNode = nodes.find((n) => n.type === 'TEXT')!
|
||||
expect(textNode.fontFamily).toBe('Roboto')
|
||||
})
|
||||
})
|
||||
|
|
@ -318,6 +318,23 @@ describe('normalizeFontFamily', () => {
|
|||
test('does not strip Variable in the middle', () => {
|
||||
expect(normalizeFontFamily('Variable Sans')).toBe('Variable Sans')
|
||||
})
|
||||
|
||||
test('strips optical size suffix (pt)', () => {
|
||||
expect(normalizeFontFamily('DM Sans 9pt')).toBe('DM Sans')
|
||||
expect(normalizeFontFamily('DM Sans 14pt')).toBe('DM Sans')
|
||||
})
|
||||
|
||||
test('strips optical size suffix (px)', () => {
|
||||
expect(normalizeFontFamily('Noto Sans 12px')).toBe('Noto Sans')
|
||||
})
|
||||
|
||||
test('strips optical size suffix (em)', () => {
|
||||
expect(normalizeFontFamily('Custom Font 1em')).toBe('Custom Font')
|
||||
})
|
||||
|
||||
test('does not strip size units in the middle', () => {
|
||||
expect(normalizeFontFamily('12pt Serif')).toBe('12pt Serif')
|
||||
})
|
||||
})
|
||||
|
||||
describe('styleToVariant', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue