diff --git a/CHANGELOG.md b/CHANGELOG.md index f22405072..31b008900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Evaluate the `**` operator in the AI and MCP `calc` tool, which its own description advertised but which the tool rejected. `calc` now accepts exactly the arithmetic it documents — `+ - * / % **`, parentheses and `min max floor ceil round abs sqrt pow` — and no longer evaluates undocumented expressions such as `random()`, factorials, trigonometry, strings, arrays, or property access. - Show Chinese, Japanese, Korean, and Arabic characters in a fallback font when the text's own font is unavailable and another font substitutes for it, instead of drawing missing-glyph boxes (#746). - Explain in the font issues banner when an installed font, such as PingFang on macOS 15 and later, stores outlines in a format OpenPencil cannot draw yet, instead of spending over a second trying to load each of its styles (#746). +- Render the Medium, Semibold, Bold, and other styles of variable fonts at their named weights instead of drawing Regular or a synthetic bold. ### Security diff --git a/packages/core/src/canvas/text/index.ts b/packages/core/src/canvas/text/index.ts index 7319b049e..96073ecc8 100644 --- a/packages/core/src/canvas/text/index.ts +++ b/packages/core/src/canvas/text/index.ts @@ -293,11 +293,18 @@ function getParagraphTextAlign( } } +/** Explicit axes win over the named-instance axes of a variable face (`implicit`). */ export function textFontVariations( - variations: SceneNode['fontVariations'] | undefined + variations: SceneNode['fontVariations'] | undefined, + implicit: SceneNode['fontVariations'] | null = null ): TextFontVariations[] | undefined { - if (!variations || variations.length === 0) return undefined - return variations.map((variation) => ({ axis: variation.axis, value: variation.value })) + const explicitAxes = new Set(variations?.map((variation) => variation.axis)) + const merged = [ + ...(implicit ?? []).filter((variation) => !explicitAxes.has(variation.axis)), + ...(variations ?? []) + ] + if (merged.length === 0) return undefined + return merged.map((variation) => ({ axis: variation.axis, value: variation.value })) } export function textFontFeatures( @@ -386,21 +393,23 @@ function pushStyleRun( const style = run.style const runLineHeight = style.lineHeight !== undefined ? style.lineHeight : node.lineHeight const runFontSize = style.fontSize ?? baseFontSize + const runFamily = style.fontFamily ?? (node.fontFamily || DEFAULT_FONT_FAMILY) + const runWeight = style.fontWeight ?? node.fontWeight + const runItalic = style.italic ?? node.italic const textStyle = new ck.TextStyle({ color: styleRunColor(ck, style, baseColor), - fontFamilies: fontFamilies( - style.fontFamily ?? (node.fontFamily || DEFAULT_FONT_FAMILY), - style.fontWeight ?? node.fontWeight, - style.italic ?? node.italic - ), + fontFamilies: fontFamilies(runFamily, runWeight, runItalic), fontSize: runFontSize, locale: styleRunLanguage(style, node), fontStyle: { - weight: { value: style.fontWeight ?? node.fontWeight } as FontWeight, - slant: (style.italic ?? node.italic) ? ck.FontSlant.Italic : ck.FontSlant.Upright + weight: { value: runWeight } as FontWeight, + slant: runItalic ? ck.FontSlant.Italic : ck.FontSlant.Upright }, - fontVariations: textFontVariations(style.fontVariations ?? node.fontVariations), + fontVariations: textFontVariations( + style.fontVariations ?? node.fontVariations, + fontManager.namedInstanceVariations(runFamily, weightToStyle(runWeight, runItalic)) + ), fontFeatures: textFontFeatures(style.fontFeatures ?? node.fontFeatures), letterSpacing: style.letterSpacing ?? (node.letterSpacing || 0), decoration: textDecorationValue(ck, style.textDecoration ?? node.textDecoration), @@ -498,7 +507,13 @@ export function buildParagraph( weight: { value: node.fontWeight } as FontWeight, slant: node.italic ? ck.FontSlant.Italic : ck.FontSlant.Upright }, - fontVariations: textFontVariations(node.fontVariations), + fontVariations: textFontVariations( + node.fontVariations, + fontManager.namedInstanceVariations( + node.fontFamily || DEFAULT_FONT_FAMILY, + weightToStyle(node.fontWeight, node.italic) + ) + ), fontFeatures: textFontFeatures(node.fontFeatures), letterSpacing: node.letterSpacing || 0, decoration: textDecorationValue(ck, node.textDecoration), diff --git a/packages/core/src/text/font/style.ts b/packages/core/src/text/font/style.ts index b95467a32..50f748d07 100644 --- a/packages/core/src/text/font/style.ts +++ b/packages/core/src/text/font/style.ts @@ -45,20 +45,4 @@ export function chooseLocalFontMatch( return undefined } -export function isVariableFont(data: ArrayBuffer): boolean { - if (data.byteLength < 12) return false - const view = new DataView(data) - const numTables = view.getUint16(4) - for (let i = 0; i < numTables && 12 + i * 16 + 4 <= data.byteLength; i++) { - const tag = String.fromCharCode( - view.getUint8(12 + i * 16), - view.getUint8(12 + i * 16 + 1), - view.getUint8(12 + i * 16 + 2), - view.getUint8(12 + i * 16 + 3) - ) - if (tag === 'fvar') return true - } - return false -} - export { weightToFigmaStyle } from '@open-pencil/fig/node-change' diff --git a/packages/core/src/text/font/variation.ts b/packages/core/src/text/font/variation.ts new file mode 100644 index 000000000..b1d0b7d2e --- /dev/null +++ b/packages/core/src/text/font/variation.ts @@ -0,0 +1,126 @@ +import { styleToWeight } from '@open-pencil/scene-graph' +import type { FontVariation } from '@open-pencil/scene-graph' + +interface TableRange { + offset: number + length: number +} + +const WINDOWS_PLATFORM = 3 +const MAC_PLATFORM = 1 +const ENGLISH_US = 0x409 +const NAME_HEADER_SIZE = 6 +const NAME_RECORD_SIZE = 12 +const FVAR_HEADER_SIZE = 16 +const AXIS_RECORD_SIZE = 20 + +function sfntTable(view: DataView, tag: string): TableRange | null { + if (view.byteLength < 12) return null + const numTables = view.getUint16(4) + for (let i = 0; i < numTables; i++) { + const record = 12 + i * 16 + if (record + 16 > view.byteLength) return null + let recordTag = '' + for (let c = 0; c < 4; c++) recordTag += String.fromCharCode(view.getUint8(record + c)) + if (recordTag !== tag) continue + const offset = view.getUint32(record + 8) + const length = view.getUint32(record + 12) + return offset + length <= view.byteLength ? { offset, length } : null + } + return null +} + +export function isVariableFont(data: ArrayBuffer): boolean { + return sfntTable(new DataView(data), 'fvar') !== null +} + +function fixed(view: DataView, offset: number): number { + return view.getInt32(offset) / 65536 +} + +function normalizeStyleName(style: string): string { + return style.toLowerCase().replace(/\s+/g, '') +} + +function fontName(view: DataView, name: TableRange, nameId: number): string | null { + const end = name.offset + name.length + if (name.length < NAME_HEADER_SIZE) return null + const count = view.getUint16(name.offset + 2) + const strings = name.offset + view.getUint16(name.offset + 4) + if (name.offset + NAME_HEADER_SIZE + count * NAME_RECORD_SIZE > end) return null + let fallback: string | null = null + for (let i = 0; i < count; i++) { + const record = name.offset + NAME_HEADER_SIZE + i * NAME_RECORD_SIZE + if (view.getUint16(record + 6) !== nameId) continue + const platform = view.getUint16(record) + const language = view.getUint16(record + 4) + const length = view.getUint16(record + 8) + const start = strings + view.getUint16(record + 10) + if (start + length > end) continue + const bytes = new Uint8Array(view.buffer, view.byteOffset + start, length) + if (platform === WINDOWS_PLATFORM) { + let value = '' + for (let c = 0; c + 1 < length; c += 2) + value += String.fromCharCode((bytes[c] << 8) | bytes[c + 1]) + if (language === ENGLISH_US) return value + fallback ??= value + } else if (platform === MAC_PLATFORM) { + fallback ??= String.fromCharCode(...bytes) + } + } + return fallback +} + +/** + * Returns the axis coordinates a variable font needs to render `style`, preferring the named + * instance with that name (for example SF Pro `Medium` is `wght` 510) and otherwise clamping the + * style's weight to the `wght` axis. Returns `null` for static fonts. + */ +export function namedInstanceVariations(data: ArrayBuffer, style: string): FontVariation[] | null { + const view = new DataView(data) + const fvar = sfntTable(view, 'fvar') + if (!fvar || fvar.length < FVAR_HEADER_SIZE) return null + + const axesOffset = fvar.offset + view.getUint16(fvar.offset + 4) + const axisCount = view.getUint16(fvar.offset + 8) + const axisSize = view.getUint16(fvar.offset + 10) + const instanceCount = view.getUint16(fvar.offset + 12) + const instanceSize = view.getUint16(fvar.offset + 14) + if (axisSize < AXIS_RECORD_SIZE || instanceSize < 4 + axisCount * 4) return null + if ( + axesOffset + axisCount * axisSize + instanceCount * instanceSize > + fvar.offset + fvar.length + ) { + return null + } + + const axes = Array.from({ length: axisCount }, (_, index) => { + const axis = axesOffset + index * axisSize + let tag = '' + for (let c = 0; c < 4; c++) tag += String.fromCharCode(view.getUint8(axis + c)) + return { tag, min: fixed(view, axis + 4), max: fixed(view, axis + 12) } + }) + + const name = sfntTable(view, 'name') + const requested = normalizeStyleName(style) + const instancesOffset = axesOffset + axisCount * axisSize + const instance = name + ? Array.from( + { length: instanceCount }, + (_, index) => instancesOffset + index * instanceSize + ).find((offset) => { + const instanceName = fontName(view, name, view.getUint16(offset)) + return instanceName !== null && normalizeStyleName(instanceName) === requested + }) + : undefined + if (instance !== undefined) { + return axes.map((axis, axisIndex) => ({ + axis: axis.tag, + value: fixed(view, instance + 4 + axisIndex * 4) + })) + } + + const weight = axes.find((axis) => axis.tag === 'wght') + if (!weight) return [] + return [{ axis: 'wght', value: Math.min(weight.max, Math.max(weight.min, styleToWeight(style))) }] +} diff --git a/packages/core/src/text/fonts.ts b/packages/core/src/text/fonts.ts index 09b5fb185..a98ac6444 100644 --- a/packages/core/src/text/fonts.ts +++ b/packages/core/src/text/fonts.ts @@ -2,19 +2,20 @@ import type { CanvasKit, TypefaceFontProvider } from 'canvaskit-wasm' -import type { SceneGraph } from '@open-pencil/scene-graph' +import type { FontVariation, SceneGraph } from '@open-pencil/scene-graph' import { DEFAULT_FONT_FAMILY, IS_BROWSER } from '#core/constants' import { chooseLocalFontMatch, - isVariableFont, normalizeFontFamily, styleToWeight, weightToStyle } from '#core/text/font/style' +import { isVariableFont, namedInstanceVariations } from '#core/text/font/variation' export * from '#core/text/font/sources' export * from '#core/text/font/style' +export * from '#core/text/font/variation' import { fontFallbackEntry } from '#core/text/fallbacks' import type { FontFallbackScript } from '#core/text/fallbacks' import { UnsupportedFontFormatError } from '#core/text/font/sources' @@ -48,6 +49,7 @@ export class FontManager { private unavailableFaces = new Map() private supplementalFamilyData = new Map() private remoteCoverage = new Map>() + private instanceVariations = new WeakMap>() private blockedNodeIds = new Set() private fontProvider: TypefaceFontProvider | null = null private fontProviders = new Set() @@ -367,6 +369,19 @@ export class FontManager { return this.loadedFamilies.get(`${family}|${style}`) ?? null } + /** Axis coordinates for `style` when the loaded face is a variable font, otherwise `null`. */ + namedInstanceVariations(family: string, style: string): FontVariation[] | null { + const data = this.loadedData(family, style) + if (!data) return null + let byStyle = this.instanceVariations.get(data) + if (!byStyle) { + byStyle = new Map() + this.instanceVariations.set(data, byStyle) + } + if (!byStyle.has(style)) byStyle.set(style, namedInstanceVariations(data, style)) + return byStyle.get(style) ?? null + } + renderFamily(family: string, _style: string): string { // CanvasKit can shape metrics but paint no glyphs for some CJK/Arabic faces registered under a // synthetic alias. Keep every shard under the font's source family; character-aware remote diff --git a/tests/e2e/fonts/variable-weights-visual.spec.ts b/tests/e2e/fonts/variable-weights-visual.spec.ts new file mode 100644 index 000000000..19dc081a9 --- /dev/null +++ b/tests/e2e/fonts/variable-weights-visual.spec.ts @@ -0,0 +1,94 @@ +import { Buffer } from 'node:buffer' + +import type { Page } from '@playwright/test' + +import type { FontManager } from '#core/text/fonts' + +import { expect, test } from '#tests/e2e/fixtures' +import { CanvasHelper } from '#tests/helpers/canvas' +import { mockFontsource } from '#tests/helpers/fonts/fontsource' +import { trackFontModuleResources } from '#tests/helpers/fonts/runtime' + +async function openEditor(page: Page): Promise { + await trackFontModuleResources(page) + // The variable fixture is installed explicitly; remote arrivals must not race it. + await mockFontsource(page, []) + await page.goto('/?test&no-chrome&no-rulers') + await new CanvasHelper(page).waitForInit() +} + +test('variable fonts render each style at its named instance', 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 + for (const id of store.graph.getNode(pageId)?.childIds.slice() ?? []) { + store.graph.deleteNode(id) + } + + // One variable file serves every style, as installed variable fonts such as SF Pro do. + const data = await fetch('/tests/fixtures/fonts/InterVariable-Latin.ttf').then((response) => + response.arrayBuffer() + ) + for (const style of ['Regular', 'Medium', 'SemiBold', 'Bold']) { + fontManager.markLoaded('Inter Variable', style, data) + } + + const samples = [ + { label: 'Regular 400', fontWeight: 400 }, + { label: 'Medium 500', fontWeight: 500 }, + { label: 'SemiBold 600', fontWeight: 600 }, + { label: 'Bold 700', fontWeight: 700 }, + { + label: 'Bold, explicit wght 300', + fontWeight: 700, + fontVariations: [{ axis: 'wght', value: 300 }] + } + ] + const ids = samples.map( + (sample, index) => + store.graph.createNode('TEXT', pageId, { + name: sample.label, + x: 40, + y: 40 + index * 56, + width: 520, + height: 44, + text: `${sample.label} — Buttons`, + fontFamily: 'Inter Variable', + fontSize: 32, + fontWeight: sample.fontWeight, + fontVariations: sample.fontVariations ?? [], + textAutoResize: 'NONE', + fills: [ + { type: 'SOLID', color: { r: 0.04, g: 0.06, b: 0.12, a: 1 }, visible: true, opacity: 1 } + ] + }).id + ) + + await store.loadFontsForNodes(ids) + store.renderer.invalidateAllPictures() + store.requestRender() + const image = await store.renderExportImage(ids, 2, 'PNG') + if (!image) throw new Error('Export failed') + return { + image: Array.from(image), + readiness: ids.map((id) => { + const node = store.graph.getNode(id) + return node ? store.renderer?.nodeFontReadiness(node) : 'missing' + }) + } + }) + + expect(result.readiness).toEqual(['ready', 'ready', 'ready', 'ready', 'ready']) + expect(Buffer.from(result.image)).toMatchSnapshot('variable-font-weights.png') +}) diff --git a/tests/e2e/fonts/variable-weights-visual.spec.ts-snapshots/variable-font-weights-openpencil-darwin.png b/tests/e2e/fonts/variable-weights-visual.spec.ts-snapshots/variable-font-weights-openpencil-darwin.png new file mode 100644 index 000000000..19b9d8ee4 Binary files /dev/null and b/tests/e2e/fonts/variable-weights-visual.spec.ts-snapshots/variable-font-weights-openpencil-darwin.png differ diff --git a/tests/engine/render/canvas/text-font-variations.test.ts b/tests/engine/render/canvas/text-font-variations.test.ts index a9318784c..097d864f2 100644 --- a/tests/engine/render/canvas/text-font-variations.test.ts +++ b/tests/engine/render/canvas/text-font-variations.test.ts @@ -49,6 +49,19 @@ describe('canvas text font variations', () => { 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', () => { diff --git a/tests/engine/text/fonts/variations.test.ts b/tests/engine/text/fonts/variations.test.ts new file mode 100644 index 000000000..f430adc71 --- /dev/null +++ b/tests/engine/text/fonts/variations.test.ts @@ -0,0 +1,198 @@ +import { describe, expect, test } from 'bun:test' + +import { FontManager } from '@open-pencil/core' + +import { isVariableFont, namedInstanceVariations } from '#core/text/font/variation' + +interface Axis { + tag: string + min: number + max: number +} + +interface Instance { + name: string + coordinates: number[] +} + +function concat(parts: Uint8Array[]): Uint8Array { + const out = new Uint8Array(parts.reduce((size, part) => size + part.length, 0)) + let offset = 0 + for (const part of parts) { + out.set(part, offset) + offset += part.length + } + return out +} + +function words(values: number[], bytes: 2 | 4): Uint8Array { + const view = new DataView(new ArrayBuffer(values.length * bytes)) + values.forEach((value, index) => + bytes === 2 ? view.setUint16(index * 2, value) : view.setInt32(index * 4, value) + ) + return new Uint8Array(view.buffer) +} + +function ascii(tag: string): Uint8Array { + return new TextEncoder().encode(tag) +} + +function nameTable(names: string[]): Uint8Array { + const strings = names.map((name) => + words( + Array.from(name, (character) => character.charCodeAt(0)), + 2 + ) + ) + const header = words([0, names.length, 6 + names.length * 12], 2) + let offset = 0 + const records = names.map((name, index) => { + const record = words([3, 1, 0x409, 256 + index, strings[index].length, offset], 2) + offset += strings[index].length + return record + }) + return concat([header, ...records, ...strings]) +} + +function fvarTable(axes: Axis[], instances: Instance[]): Uint8Array { + const header = words([1, 0, 16, 2, axes.length, 20, instances.length, 4 + axes.length * 4], 2) + const axisRecords = axes.map((axis) => + concat([ + ascii(axis.tag), + words( + [axis.min, axis.min, axis.max].map((value) => value * 65536), + 4 + ), + words([0, 0], 2) + ]) + ) + const instanceRecords = instances.map((instance, index) => + concat([ + words([256 + index, 0], 2), + words( + instance.coordinates.map((value) => value * 65536), + 4 + ) + ]) + ) + return concat([header, ...axisRecords, ...instanceRecords]) +} + +function variableFont(axes: Axis[], instances: Instance[]): ArrayBuffer { + const tables: Array<[string, Uint8Array]> = [ + ['fvar', fvarTable(axes, instances)], + ['name', nameTable(instances.map((instance) => instance.name))] + ] + const directory = concat([ + words([0x0001, 0x0000, tables.length, 0, 0, 0], 2), + ...tables.map(([tag, data], index) => { + const offset = + 12 + tables.length * 16 + tables.slice(0, index).reduce((size, [, t]) => size + t.length, 0) + return concat([ascii(tag), words([0, offset, data.length], 4)]) + }) + ]) + return concat([directory, ...tables.map(([, data]) => data)]).buffer as ArrayBuffer +} + +const SF_PRO_LIKE = variableFont( + [ + { tag: 'wdth', min: 30, max: 150 }, + { tag: 'wght', min: 1, max: 1000 } + ], + [ + { name: 'Condensed Medium', coordinates: [60, 540] }, + { name: 'Regular', coordinates: [100, 400] }, + { name: 'Medium', coordinates: [100, 510] }, + { name: 'Semibold', coordinates: [100, 590] }, + { name: 'Bold Italic', coordinates: [100, 700] } + ] +) + +describe('namedInstanceVariations', () => { + test('uses the coordinates of the named instance matching the style', () => { + expect(namedInstanceVariations(SF_PRO_LIKE, 'Medium')).toEqual([ + { axis: 'wdth', value: 100 }, + { axis: 'wght', value: 510 } + ]) + expect(namedInstanceVariations(SF_PRO_LIKE, 'Bold Italic')).toEqual([ + { axis: 'wdth', value: 100 }, + { axis: 'wght', value: 700 } + ]) + }) + + test('matches instance names regardless of case and spacing', () => { + expect(namedInstanceVariations(SF_PRO_LIKE, 'SemiBold')).toEqual([ + { axis: 'wdth', value: 100 }, + { axis: 'wght', value: 590 } + ]) + }) + + test('clamps the style weight to the wght axis without a matching instance', () => { + expect(namedInstanceVariations(SF_PRO_LIKE, 'Black')).toEqual([{ axis: 'wght', value: 900 }]) + const narrow = variableFont([{ tag: 'wght', min: 300, max: 600 }], []) + expect(namedInstanceVariations(narrow, 'Bold')).toEqual([{ axis: 'wght', value: 600 }]) + }) + + test('returns null for static fonts', () => { + const header = words([0x0001, 0x0000, 0, 0, 0, 0], 2) + expect(namedInstanceVariations(header.buffer as ArrayBuffer, 'Medium')).toBeNull() + expect(isVariableFont(header.buffer as ArrayBuffer)).toBe(false) + expect(isVariableFont(SF_PRO_LIKE)).toBe(true) + }) +}) + +describe('namedInstanceVariations with malformed fonts', () => { + function patched(patch: (view: DataView, tableOffset: (tag: string) => number) => void) { + const data = SF_PRO_LIKE.slice(0) + const view = new DataView(data) + const tableRecord = (tag: string) => { + for (let i = 0; i < view.getUint16(4); i++) { + const record = 12 + i * 16 + const recordTag = String.fromCharCode(...new Uint8Array(data, record, 4)) + if (recordTag === tag) return record + } + throw new Error(`Missing ${tag}`) + } + patch(view, tableRecord) + return data + } + + test('skips name records that point outside the font', () => { + const data = patched((view, tableRecord) => { + const name = view.getUint32(tableRecord('name') + 8) + view.setUint16(name + 6 + 10, 0xfff0) + }) + expect( + namedInstanceVariations(data, 'Condensed Medium')?.map((variation) => variation.axis) + ).toEqual(['wght']) + expect(namedInstanceVariations(data, 'Medium')).toEqual([ + { axis: 'wdth', value: 100 }, + { axis: 'wght', value: 510 } + ]) + }) + + test('ignores fvar tables shorter than their header', () => { + const data = patched((view, tableRecord) => view.setUint32(tableRecord('fvar') + 12, 10)) + expect(namedInstanceVariations(data, 'Medium')).toBeNull() + }) + + test('ignores fvar tables with undersized axis records', () => { + const data = patched((view, tableRecord) => { + const fvar = view.getUint32(tableRecord('fvar') + 8) + view.setUint16(fvar + 10, 8) + }) + expect(namedInstanceVariations(data, 'Medium')).toBeNull() + }) +}) + +describe('FontManager.namedInstanceVariations', () => { + test('resolves variations from the face loaded for a style', () => { + const manager = new FontManager() + manager.markLoaded('SF Pro', 'Medium', SF_PRO_LIKE) + expect(manager.namedInstanceVariations('SF Pro', 'Medium')).toEqual([ + { axis: 'wdth', value: 100 }, + { axis: 'wght', value: 510 } + ]) + expect(manager.namedInstanceVariations('SF Pro', 'Bold')).toBeNull() + }) +}) diff --git a/tests/fixtures/fonts/Inter-OFL.txt b/tests/fixtures/fonts/Inter-OFL.txt new file mode 100644 index 000000000..9b2ca37b3 --- /dev/null +++ b/tests/fixtures/fonts/Inter-OFL.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +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: +http://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 AND 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. diff --git a/tests/fixtures/fonts/InterVariable-Latin.ttf b/tests/fixtures/fonts/InterVariable-Latin.ttf new file mode 100644 index 000000000..edb829b03 --- /dev/null +++ b/tests/fixtures/fonts/InterVariable-Latin.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:768df651a9fd629b069e707a0ffb6880291650c8bd8f997e9fee83c2d6b2197a +size 166368