@liiift-studio/sanity-font-manager 2.3.19 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +437 -437
- package/dist/index.js +103 -48
- package/dist/index.mjs +103 -48
- package/package.json +85 -85
- package/src/components/BatchUploadFonts.jsx +640 -639
- package/src/components/FontScriptUploaderComponent.jsx +463 -463
- package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
- package/src/components/KeyValueInput.jsx +95 -95
- package/src/components/KeyValueReferenceInput.jsx +254 -254
- package/src/components/NestedObjectArraySelector.jsx +146 -146
- package/src/components/PriceInput.jsx +26 -26
- package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
- package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
- package/src/components/SetOTF.jsx +87 -87
- package/src/components/SingleUploaderTool.jsx +673 -673
- package/src/components/StatusDisplay.jsx +26 -26
- package/src/components/StyleCountInput.jsx +16 -16
- package/src/components/UpdateScriptsComponent.jsx +76 -76
- package/src/components/UploadButton.jsx +43 -43
- package/src/components/UploadScriptsComponent.jsx +537 -537
- package/src/components/VariableInstanceReferencesInput.jsx +190 -190
- package/src/hooks/useNestedObjects.js +92 -92
- package/src/hooks/useSanityClient.js +9 -9
- package/src/index.js +70 -70
- package/src/schema/openTypeField.js +1945 -1945
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +268 -268
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/generateCssFile.js +205 -205
- package/src/utils/generateFontData.js +145 -145
- package/src/utils/generateFontFile.js +38 -38
- package/src/utils/generateKeywords.js +185 -185
- package/src/utils/generateSubset.js +45 -45
- package/src/utils/getEmptyFontKit.js +99 -99
- package/src/utils/parseVariableFontInstances.js +211 -211
- package/src/utils/processFontFiles.js +487 -477
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +149 -160
- package/src/utils/uploadFontFiles.js +115 -26
- package/src/utils/utils.js +24 -24
|
@@ -1,205 +1,205 @@
|
|
|
1
|
-
// Builds a @font-face CSS file from a WOFF2 blob — URL or base64 src, variable font axis descriptors, metric-tuned fallback @font-face for CLS reduction
|
|
2
|
-
import base64 from 'base-64';
|
|
3
|
-
import { Buffer } from 'buffer';
|
|
4
|
-
import * as fontkit from 'fontkit';
|
|
5
|
-
|
|
6
|
-
function _arrayBufferToBase64(buffer) {
|
|
7
|
-
var binary = '';
|
|
8
|
-
var bytes = new Uint8Array(buffer);
|
|
9
|
-
var len = bytes.byteLength;
|
|
10
|
-
for (var i = 0; i < len; i++) {
|
|
11
|
-
binary += String.fromCharCode(bytes[i]);
|
|
12
|
-
}
|
|
13
|
-
return base64.encode(binary);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Reads variable axes from a fontkit font object and returns:
|
|
18
|
-
* { descriptors, skipped }
|
|
19
|
-
* where `descriptors` is the CSS string for the @font-face block and
|
|
20
|
-
* `skipped` lists axis tags that have no CSS descriptor (opsz, custom axes, etc.)
|
|
21
|
-
* so callers can surface them in comments.
|
|
22
|
-
*
|
|
23
|
-
* Edge cases handled:
|
|
24
|
-
* - Degenerate axes (min === max): skipped — no actual variation range
|
|
25
|
-
* - ital with max === 0: skipped — axis exists but font has no italic
|
|
26
|
-
* - slnt ordering: sorted ascending as CSS spec requires
|
|
27
|
-
* - ital + slnt coexistence: slnt takes priority (more expressive)
|
|
28
|
-
* - min > max from corrupt font data: clamped with Math.min/Math.max
|
|
29
|
-
* @param {Object} font - fontkit font instance
|
|
30
|
-
* @returns {{ descriptors: string, skipped: string[] }}
|
|
31
|
-
*/
|
|
32
|
-
export function buildVFDescriptors(font) {
|
|
33
|
-
const cssAxes = {}
|
|
34
|
-
const skipped = []
|
|
35
|
-
try {
|
|
36
|
-
const va = font.variationAxes
|
|
37
|
-
if (!va) return { descriptors: '', skipped: [] }
|
|
38
|
-
|
|
39
|
-
for (const [tag, axis] of Object.entries(va)) {
|
|
40
|
-
const lo = Math.min(axis.min, axis.max)
|
|
41
|
-
const hi = Math.max(axis.min, axis.max)
|
|
42
|
-
|
|
43
|
-
// Skip degenerate axes — no actual range
|
|
44
|
-
if (lo === hi) { skipped.push(tag); continue }
|
|
45
|
-
|
|
46
|
-
if (tag === 'wght') {
|
|
47
|
-
cssAxes['font-weight'] = `${lo} ${hi}`
|
|
48
|
-
} else if (tag === 'wdth') {
|
|
49
|
-
cssAxes['font-stretch'] = `${lo}% ${hi}%`
|
|
50
|
-
} else if (tag === 'slnt') {
|
|
51
|
-
// slnt: ascending order required by CSS Fonts Level 4
|
|
52
|
-
cssAxes['font-style'] = `oblique ${lo}deg ${hi}deg`
|
|
53
|
-
} else if (tag === 'ital' && !cssAxes['font-style']) {
|
|
54
|
-
// ital: only emit if font actually has italic range; slnt takes priority
|
|
55
|
-
if (hi > 0) cssAxes['font-style'] = 'italic'
|
|
56
|
-
else skipped.push(tag)
|
|
57
|
-
} else {
|
|
58
|
-
// opsz, GRAD, XTRA, XHGT and all other custom axes have no CSS @font-face descriptor
|
|
59
|
-
skipped.push(tag)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
} catch (_) {
|
|
63
|
-
// axes unreadable — no descriptors
|
|
64
|
-
}
|
|
65
|
-
const descriptors = Object.entries(cssAxes).map(([k, v]) => `${k}:${v}`).join(';') + (Object.keys(cssAxes).length ? ';' : '')
|
|
66
|
-
return { descriptors, skipped }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Cross-platform fallback font stacks by category.
|
|
70
|
-
// Multiple local() sources ensure the first available system font is used.
|
|
71
|
-
// Liberation Sans covers Linux; Roboto covers Android; Georgia is universal for serifs.
|
|
72
|
-
const FALLBACK_STACKS = {
|
|
73
|
-
'sans-serif': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
|
|
74
|
-
'serif': "local('Georgia'), local('Times New Roman'), local('Times')",
|
|
75
|
-
'monospace': "local('Courier New'), local('Courier'), local('Menlo'), local('Monaco')",
|
|
76
|
-
// Display and script fonts have no universally suitable system fallback; default to sans-serif
|
|
77
|
-
'default': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// OS/2 sFamilyClass high byte → FALLBACK_STACKS key.
|
|
81
|
-
// Classes 1–5,7 = serif variants; 8 = sans-serif; 9 = ornamental; 10 = script; 12 = symbolic.
|
|
82
|
-
const FAMILY_CLASS_MAP = {
|
|
83
|
-
1: 'serif', 2: 'serif', 3: 'serif', 4: 'serif', 5: 'serif', 7: 'serif',
|
|
84
|
-
8: 'sans-serif',
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// Darden Studio fonts have sFamilyClass: 0 (No Classification) in their OS/2 table,
|
|
88
|
-
// so name-based matching is used as the primary signal.
|
|
89
|
-
const SERIF_NAMES = /jubilat|corundum|dapifer|birra|daith/i;
|
|
90
|
-
const SANS_NAMES = /halyard|gamay|omnes|kit/i;
|
|
91
|
-
|
|
92
|
-
/** Detects font category from the font name first, then OS/2 sFamilyClass as fallback */
|
|
93
|
-
function detectFontCategory(font, fontName) {
|
|
94
|
-
if (fontName && SERIF_NAMES.test(fontName)) return 'serif';
|
|
95
|
-
if (fontName && SANS_NAMES.test(fontName)) return 'sans-serif';
|
|
96
|
-
try {
|
|
97
|
-
// fontkit v2: font['OS/2'] exposes the parsed OS/2 table directly
|
|
98
|
-
const familyClass = font['OS/2']?.sFamilyClass ?? 0;
|
|
99
|
-
const highByte = (familyClass >> 8) & 0xFF;
|
|
100
|
-
return FAMILY_CLASS_MAP[highByte] ?? 'default';
|
|
101
|
-
} catch {
|
|
102
|
-
return 'default';
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Extracts metric override percentages and detects the category fallback stack from a font ArrayBuffer */
|
|
107
|
-
function calcFallbackData(arrayBuffer, fontName) {
|
|
108
|
-
try {
|
|
109
|
-
let font = fontkit.create(Buffer.from(arrayBuffer));
|
|
110
|
-
let upm = font.unitsPerEm;
|
|
111
|
-
let category = detectFontCategory(font, fontName);
|
|
112
|
-
return {
|
|
113
|
-
fallbackSrc: FALLBACK_STACKS[category],
|
|
114
|
-
ascentOverride: `${(font.ascent / upm * 100).toFixed(2)}%`,
|
|
115
|
-
descentOverride: `${(Math.abs(font.descent) / upm * 100).toFixed(2)}%`,
|
|
116
|
-
lineGapOverride: `${(font.lineGap / upm * 100).toFixed(2)}%`,
|
|
117
|
-
};
|
|
118
|
-
} catch (err) {
|
|
119
|
-
console.error('Failed to extract fallback font data:', err);
|
|
120
|
-
return {
|
|
121
|
-
fallbackSrc: FALLBACK_STACKS['default'],
|
|
122
|
-
ascentOverride: '100%',
|
|
123
|
-
descentOverride: '0%',
|
|
124
|
-
lineGapOverride: '0%',
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export default async function generateCssFile({
|
|
130
|
-
woff2File,
|
|
131
|
-
fileInput,
|
|
132
|
-
language = null,
|
|
133
|
-
fileName,
|
|
134
|
-
fontName,
|
|
135
|
-
variableFont,
|
|
136
|
-
weight,
|
|
137
|
-
style = 'Normal',
|
|
138
|
-
client,
|
|
139
|
-
}) {
|
|
140
|
-
try {
|
|
141
|
-
// Read the file once; reuse the same buffer for base64 encoding and fontkit analysis
|
|
142
|
-
let arrayBuffer = await woff2File.arrayBuffer();
|
|
143
|
-
let b64 = _arrayBufferToBase64(arrayBuffer);
|
|
144
|
-
let fontkitFont = fontkit.create(Buffer.from(arrayBuffer));
|
|
145
|
-
let { fallbackSrc, ascentOverride, descentOverride, lineGapOverride } = calcFallbackData(arrayBuffer, fontName);
|
|
146
|
-
|
|
147
|
-
let cssString;
|
|
148
|
-
if (variableFont) {
|
|
149
|
-
let { descriptors, skipped } = buildVFDescriptors(fontkitFont);
|
|
150
|
-
// Axes with no CSS @font-face descriptor (opsz needs font-optical-sizing:auto on elements;
|
|
151
|
-
// custom axes like GRAD require CSS @font-face syntax not yet standardised).
|
|
152
|
-
let skipComment = skipped.length
|
|
153
|
-
? `/* axes present but have no @font-face descriptor: ${skipped.join(', ')}` +
|
|
154
|
-
(skipped.includes('opsz') ? ' — add font-optical-sizing:auto to your element CSS' : '') +
|
|
155
|
-
' */'
|
|
156
|
-
: ''
|
|
157
|
-
cssString = `${skipComment}@font-face{font-family:'${fontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2-variations');${descriptors}font-display:swap;}`;
|
|
158
|
-
} else {
|
|
159
|
-
let fontStyle = style === 'Italic' ? 'italic' : 'normal';
|
|
160
|
-
cssString = `@font-face{font-family:'${fontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2');font-weight:${weight};font-style:${fontStyle};font-display:swap;}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Fallback @font-face: tunes a category-appropriate system font to match the custom font's
|
|
164
|
-
// line metrics (ascent/descent/lineGap), reducing layout shift while the custom font loads.
|
|
165
|
-
// Customers who reference fontSrc in their own projects get this fallback automatically.
|
|
166
|
-
let fallbackCssString = `@font-face{font-family:'${fontName} Fallback';src:${fallbackSrc};ascent-override:${ascentOverride};descent-override:${descentOverride};line-gap-override:${lineGapOverride};}`;
|
|
167
|
-
|
|
168
|
-
let uploadBuffer = Buffer.from(cssString + fallbackCssString, 'utf-8');
|
|
169
|
-
let doc = await client.assets.upload('file', uploadBuffer, { filename: fileName + '.css' });
|
|
170
|
-
|
|
171
|
-
let newFileInput = language == null ?
|
|
172
|
-
{
|
|
173
|
-
...fileInput,
|
|
174
|
-
css: {
|
|
175
|
-
_type: 'file',
|
|
176
|
-
asset: {
|
|
177
|
-
_type: 'reference',
|
|
178
|
-
_ref: doc._id
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
:
|
|
183
|
-
{
|
|
184
|
-
...fileInput,
|
|
185
|
-
[language]: {
|
|
186
|
-
...fileInput[language],
|
|
187
|
-
css: {
|
|
188
|
-
_type: 'file',
|
|
189
|
-
asset: {
|
|
190
|
-
_type: 'reference',
|
|
191
|
-
_ref: doc._id
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return newFileInput;
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
catch (err) {
|
|
201
|
-
console.error(err);
|
|
202
|
-
throw err;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
}
|
|
1
|
+
// Builds a @font-face CSS file from a WOFF2 blob — URL or base64 src, variable font axis descriptors, metric-tuned fallback @font-face for CLS reduction
|
|
2
|
+
import base64 from 'base-64';
|
|
3
|
+
import { Buffer } from 'buffer';
|
|
4
|
+
import * as fontkit from 'fontkit';
|
|
5
|
+
|
|
6
|
+
function _arrayBufferToBase64(buffer) {
|
|
7
|
+
var binary = '';
|
|
8
|
+
var bytes = new Uint8Array(buffer);
|
|
9
|
+
var len = bytes.byteLength;
|
|
10
|
+
for (var i = 0; i < len; i++) {
|
|
11
|
+
binary += String.fromCharCode(bytes[i]);
|
|
12
|
+
}
|
|
13
|
+
return base64.encode(binary);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads variable axes from a fontkit font object and returns:
|
|
18
|
+
* { descriptors, skipped }
|
|
19
|
+
* where `descriptors` is the CSS string for the @font-face block and
|
|
20
|
+
* `skipped` lists axis tags that have no CSS descriptor (opsz, custom axes, etc.)
|
|
21
|
+
* so callers can surface them in comments.
|
|
22
|
+
*
|
|
23
|
+
* Edge cases handled:
|
|
24
|
+
* - Degenerate axes (min === max): skipped — no actual variation range
|
|
25
|
+
* - ital with max === 0: skipped — axis exists but font has no italic
|
|
26
|
+
* - slnt ordering: sorted ascending as CSS spec requires
|
|
27
|
+
* - ital + slnt coexistence: slnt takes priority (more expressive)
|
|
28
|
+
* - min > max from corrupt font data: clamped with Math.min/Math.max
|
|
29
|
+
* @param {Object} font - fontkit font instance
|
|
30
|
+
* @returns {{ descriptors: string, skipped: string[] }}
|
|
31
|
+
*/
|
|
32
|
+
export function buildVFDescriptors(font) {
|
|
33
|
+
const cssAxes = {}
|
|
34
|
+
const skipped = []
|
|
35
|
+
try {
|
|
36
|
+
const va = font.variationAxes
|
|
37
|
+
if (!va) return { descriptors: '', skipped: [] }
|
|
38
|
+
|
|
39
|
+
for (const [tag, axis] of Object.entries(va)) {
|
|
40
|
+
const lo = Math.min(axis.min, axis.max)
|
|
41
|
+
const hi = Math.max(axis.min, axis.max)
|
|
42
|
+
|
|
43
|
+
// Skip degenerate axes — no actual range
|
|
44
|
+
if (lo === hi) { skipped.push(tag); continue }
|
|
45
|
+
|
|
46
|
+
if (tag === 'wght') {
|
|
47
|
+
cssAxes['font-weight'] = `${lo} ${hi}`
|
|
48
|
+
} else if (tag === 'wdth') {
|
|
49
|
+
cssAxes['font-stretch'] = `${lo}% ${hi}%`
|
|
50
|
+
} else if (tag === 'slnt') {
|
|
51
|
+
// slnt: ascending order required by CSS Fonts Level 4
|
|
52
|
+
cssAxes['font-style'] = `oblique ${lo}deg ${hi}deg`
|
|
53
|
+
} else if (tag === 'ital' && !cssAxes['font-style']) {
|
|
54
|
+
// ital: only emit if font actually has italic range; slnt takes priority
|
|
55
|
+
if (hi > 0) cssAxes['font-style'] = 'italic'
|
|
56
|
+
else skipped.push(tag)
|
|
57
|
+
} else {
|
|
58
|
+
// opsz, GRAD, XTRA, XHGT and all other custom axes have no CSS @font-face descriptor
|
|
59
|
+
skipped.push(tag)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (_) {
|
|
63
|
+
// axes unreadable — no descriptors
|
|
64
|
+
}
|
|
65
|
+
const descriptors = Object.entries(cssAxes).map(([k, v]) => `${k}:${v}`).join(';') + (Object.keys(cssAxes).length ? ';' : '')
|
|
66
|
+
return { descriptors, skipped }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cross-platform fallback font stacks by category.
|
|
70
|
+
// Multiple local() sources ensure the first available system font is used.
|
|
71
|
+
// Liberation Sans covers Linux; Roboto covers Android; Georgia is universal for serifs.
|
|
72
|
+
const FALLBACK_STACKS = {
|
|
73
|
+
'sans-serif': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
|
|
74
|
+
'serif': "local('Georgia'), local('Times New Roman'), local('Times')",
|
|
75
|
+
'monospace': "local('Courier New'), local('Courier'), local('Menlo'), local('Monaco')",
|
|
76
|
+
// Display and script fonts have no universally suitable system fallback; default to sans-serif
|
|
77
|
+
'default': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// OS/2 sFamilyClass high byte → FALLBACK_STACKS key.
|
|
81
|
+
// Classes 1–5,7 = serif variants; 8 = sans-serif; 9 = ornamental; 10 = script; 12 = symbolic.
|
|
82
|
+
const FAMILY_CLASS_MAP = {
|
|
83
|
+
1: 'serif', 2: 'serif', 3: 'serif', 4: 'serif', 5: 'serif', 7: 'serif',
|
|
84
|
+
8: 'sans-serif',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Darden Studio fonts have sFamilyClass: 0 (No Classification) in their OS/2 table,
|
|
88
|
+
// so name-based matching is used as the primary signal.
|
|
89
|
+
const SERIF_NAMES = /jubilat|corundum|dapifer|birra|daith/i;
|
|
90
|
+
const SANS_NAMES = /halyard|gamay|omnes|kit/i;
|
|
91
|
+
|
|
92
|
+
/** Detects font category from the font name first, then OS/2 sFamilyClass as fallback */
|
|
93
|
+
function detectFontCategory(font, fontName) {
|
|
94
|
+
if (fontName && SERIF_NAMES.test(fontName)) return 'serif';
|
|
95
|
+
if (fontName && SANS_NAMES.test(fontName)) return 'sans-serif';
|
|
96
|
+
try {
|
|
97
|
+
// fontkit v2: font['OS/2'] exposes the parsed OS/2 table directly
|
|
98
|
+
const familyClass = font['OS/2']?.sFamilyClass ?? 0;
|
|
99
|
+
const highByte = (familyClass >> 8) & 0xFF;
|
|
100
|
+
return FAMILY_CLASS_MAP[highByte] ?? 'default';
|
|
101
|
+
} catch {
|
|
102
|
+
return 'default';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Extracts metric override percentages and detects the category fallback stack from a font ArrayBuffer */
|
|
107
|
+
function calcFallbackData(arrayBuffer, fontName) {
|
|
108
|
+
try {
|
|
109
|
+
let font = fontkit.create(Buffer.from(arrayBuffer));
|
|
110
|
+
let upm = font.unitsPerEm;
|
|
111
|
+
let category = detectFontCategory(font, fontName);
|
|
112
|
+
return {
|
|
113
|
+
fallbackSrc: FALLBACK_STACKS[category],
|
|
114
|
+
ascentOverride: `${(font.ascent / upm * 100).toFixed(2)}%`,
|
|
115
|
+
descentOverride: `${(Math.abs(font.descent) / upm * 100).toFixed(2)}%`,
|
|
116
|
+
lineGapOverride: `${(font.lineGap / upm * 100).toFixed(2)}%`,
|
|
117
|
+
};
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error('Failed to extract fallback font data:', err);
|
|
120
|
+
return {
|
|
121
|
+
fallbackSrc: FALLBACK_STACKS['default'],
|
|
122
|
+
ascentOverride: '100%',
|
|
123
|
+
descentOverride: '0%',
|
|
124
|
+
lineGapOverride: '0%',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default async function generateCssFile({
|
|
130
|
+
woff2File,
|
|
131
|
+
fileInput,
|
|
132
|
+
language = null,
|
|
133
|
+
fileName,
|
|
134
|
+
fontName,
|
|
135
|
+
variableFont,
|
|
136
|
+
weight,
|
|
137
|
+
style = 'Normal',
|
|
138
|
+
client,
|
|
139
|
+
}) {
|
|
140
|
+
try {
|
|
141
|
+
// Read the file once; reuse the same buffer for base64 encoding and fontkit analysis
|
|
142
|
+
let arrayBuffer = await woff2File.arrayBuffer();
|
|
143
|
+
let b64 = _arrayBufferToBase64(arrayBuffer);
|
|
144
|
+
let fontkitFont = fontkit.create(Buffer.from(arrayBuffer));
|
|
145
|
+
let { fallbackSrc, ascentOverride, descentOverride, lineGapOverride } = calcFallbackData(arrayBuffer, fontName);
|
|
146
|
+
|
|
147
|
+
let cssString;
|
|
148
|
+
if (variableFont) {
|
|
149
|
+
let { descriptors, skipped } = buildVFDescriptors(fontkitFont);
|
|
150
|
+
// Axes with no CSS @font-face descriptor (opsz needs font-optical-sizing:auto on elements;
|
|
151
|
+
// custom axes like GRAD require CSS @font-face syntax not yet standardised).
|
|
152
|
+
let skipComment = skipped.length
|
|
153
|
+
? `/* axes present but have no @font-face descriptor: ${skipped.join(', ')}` +
|
|
154
|
+
(skipped.includes('opsz') ? ' — add font-optical-sizing:auto to your element CSS' : '') +
|
|
155
|
+
' */'
|
|
156
|
+
: ''
|
|
157
|
+
cssString = `${skipComment}@font-face{font-family:'${fontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2-variations');${descriptors}font-display:swap;}`;
|
|
158
|
+
} else {
|
|
159
|
+
let fontStyle = style === 'Italic' ? 'italic' : 'normal';
|
|
160
|
+
cssString = `@font-face{font-family:'${fontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2');font-weight:${weight};font-style:${fontStyle};font-display:swap;}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Fallback @font-face: tunes a category-appropriate system font to match the custom font's
|
|
164
|
+
// line metrics (ascent/descent/lineGap), reducing layout shift while the custom font loads.
|
|
165
|
+
// Customers who reference fontSrc in their own projects get this fallback automatically.
|
|
166
|
+
let fallbackCssString = `@font-face{font-family:'${fontName} Fallback';src:${fallbackSrc};ascent-override:${ascentOverride};descent-override:${descentOverride};line-gap-override:${lineGapOverride};}`;
|
|
167
|
+
|
|
168
|
+
let uploadBuffer = Buffer.from(cssString + fallbackCssString, 'utf-8');
|
|
169
|
+
let doc = await client.assets.upload('file', uploadBuffer, { filename: fileName + '.css' });
|
|
170
|
+
|
|
171
|
+
let newFileInput = language == null ?
|
|
172
|
+
{
|
|
173
|
+
...fileInput,
|
|
174
|
+
css: {
|
|
175
|
+
_type: 'file',
|
|
176
|
+
asset: {
|
|
177
|
+
_type: 'reference',
|
|
178
|
+
_ref: doc._id
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
:
|
|
183
|
+
{
|
|
184
|
+
...fileInput,
|
|
185
|
+
[language]: {
|
|
186
|
+
...fileInput[language],
|
|
187
|
+
css: {
|
|
188
|
+
_type: 'file',
|
|
189
|
+
asset: {
|
|
190
|
+
_type: 'reference',
|
|
191
|
+
_ref: doc._id
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return newFileInput;
|
|
198
|
+
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
console.error(err);
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
}
|