labelEditorFonts.ts
7.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import type { SystemLabelTemplate } from './print/types/printer'
/** 与 Web 管理端 labelTemplate.ts 保持一致 */
export const LABEL_EDITOR_FONT_FAMILY = 'FreightSans Bold'
export const LABEL_EDITOR_FONT_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [
{ value: 'FreightSans Bold', label: 'FreightSans Bold' },
{ value: 'Roboto', label: 'Roboto' },
{ value: 'Open Sans', label: 'Open Sans' },
{ value: 'Lato', label: 'Lato' },
{ value: 'Tinos', label: 'Tinos' },
{ value: 'Roboto Mono', label: 'Roboto Mono' },
]
export const LABEL_EDITOR_BUNDLED_FONT_FAMILIES = new Set(
LABEL_EDITOR_FONT_OPTIONS.map((item) => item.value),
)
const LEGACY_FONT_ALIASES: Record<string, string> = {
arial: 'Roboto',
'arial, sans-serif': 'Roboto',
helvetica: 'Roboto',
'helvetica, sans-serif': 'Roboto',
'times new roman': 'Tinos',
'courier new': 'Roboto Mono',
verdana: 'Open Sans',
georgia: 'Tinos',
}
type FontVariantSpec = {
file: string
weight: 'normal' | 'bold'
style: 'normal' | 'italic'
}
type FontPackageSpec = {
family: string
dir: string
variants: FontVariantSpec[]
}
/** static/fonts 下已打包字体(与 Web src/assets/fonts 对齐) */
export const LABEL_EDITOR_FONT_PACKAGES: FontPackageSpec[] = [
{
family: 'FreightSans Bold',
dir: 'freight-sans-bold',
variants: [{ file: 'FreightSans-Bold.ttf', weight: 'bold', style: 'normal' }],
},
{
family: 'Roboto',
dir: 'roboto',
variants: [
{ file: 'roboto-latin-400-normal.woff2', weight: 'normal', style: 'normal' },
{ file: 'roboto-latin-700-normal.woff2', weight: 'bold', style: 'normal' },
{ file: 'roboto-latin-400-italic.woff2', weight: 'normal', style: 'italic' },
],
},
{
family: 'Open Sans',
dir: 'open-sans',
variants: [
{ file: 'open-sans-latin-400-normal.woff2', weight: 'normal', style: 'normal' },
{ file: 'open-sans-latin-700-normal.woff2', weight: 'bold', style: 'normal' },
{ file: 'open-sans-latin-400-italic.woff2', weight: 'normal', style: 'italic' },
],
},
{
family: 'Lato',
dir: 'lato',
variants: [
{ file: 'lato-latin-400-normal.woff2', weight: 'normal', style: 'normal' },
{ file: 'lato-latin-700-normal.woff2', weight: 'bold', style: 'normal' },
{ file: 'lato-latin-400-italic.woff2', weight: 'normal', style: 'italic' },
],
},
{
family: 'Tinos',
dir: 'tinos',
variants: [
{ file: 'tinos-latin-400-normal.woff2', weight: 'normal', style: 'normal' },
{ file: 'tinos-latin-700-normal.woff2', weight: 'bold', style: 'normal' },
{ file: 'tinos-latin-400-italic.woff2', weight: 'normal', style: 'italic' },
],
},
{
family: 'Roboto Mono',
dir: 'roboto-mono',
variants: [
{ file: 'roboto-mono-latin-400-normal.woff2', weight: 'normal', style: 'normal' },
{ file: 'roboto-mono-latin-700-normal.woff2', weight: 'bold', style: 'normal' },
{ file: 'roboto-mono-latin-400-italic.woff2', weight: 'normal', style: 'italic' },
],
},
]
const loadedFaceKeys = new Set<string>()
const loadedFamilies = new Set<string>()
let preloadAllPromise: Promise<void> | null = null
export function normalizeLabelEditorFontFamily(raw: unknown): string | null {
const text = String(raw ?? '').trim()
if (!text) return null
const alias = LEGACY_FONT_ALIASES[text.toLowerCase()]
if (alias) return alias
if (LABEL_EDITOR_BUNDLED_FONT_FAMILIES.has(text)) return text
return null
}
export function resolveLabelEditorFontFamily(
config: Record<string, unknown> | null | undefined,
): string {
return (
normalizeLabelEditorFontFamily(config?.fontFamily ?? config?.FontFamily)
?? LABEL_EDITOR_FONT_FAMILY
)
}
function fontPackageByFamily(family: string): FontPackageSpec | undefined {
return LABEL_EDITOR_FONT_PACKAGES.find((item) => item.family === family)
}
/** static/fonts/{dir}/{file} → 各端可访问的本地 URL */
export function resolveStaticFontFileUrl(dir: string, file: string): string {
const rel = `static/fonts/${dir}/${file}`.replace(/\\/g, '/')
// #ifdef APP-PLUS
try {
if (typeof plus !== 'undefined' && plus.io?.convertLocalFileSystemURL) {
return plus.io.convertLocalFileSystemURL(`_www/${rel}`)
}
} catch (_) {
/* fall through */
}
// #endif
return `/${rel}`
}
/** Android 原生位图文字:Typeface.createFromFile 用的绝对路径 */
export function resolveAndroidFontFilePath(
family: string,
bold: boolean,
italic: boolean,
): string | null {
const pkg = fontPackageByFamily(family)
if (!pkg) return null
const weight = bold ? 'bold' : 'normal'
const style = italic ? 'italic' : 'normal'
let variant = pkg.variants.find((v) => v.weight === weight && v.style === style)
if (!variant && bold) {
variant = pkg.variants.find((v) => v.weight === 'bold' && v.style === 'normal')
}
if (!variant) {
variant = pkg.variants.find((v) => v.weight === 'normal' && v.style === 'normal')
}
if (!variant) return null
return resolveStaticFontFileUrl(pkg.dir, variant.file)
}
function loadFontFaceOnce(
family: string,
dir: string,
variant: FontVariantSpec,
): Promise<void> {
const key = `${family}|${variant.weight}|${variant.style}|${variant.file}`
if (loadedFaceKeys.has(key)) return Promise.resolve()
const url = resolveStaticFontFileUrl(dir, variant.file)
return new Promise((resolve) => {
uni.loadFontFace({
global: true,
family,
source: `url("${url}")`,
desc: {
style: variant.style,
weight: variant.weight,
},
success: () => {
loadedFaceKeys.add(key)
loadedFamilies.add(family)
if (variant.style === 'italic') loadedItalicFamilies.add(family)
resolve()
},
fail: (err) => {
console.warn('[labelEditorFonts] loadFontFace failed', family, variant, err)
resolve()
},
})
})
}
export function isLabelEditorFontFamilyLoaded(family: string): boolean {
return loadedFamilies.has(family)
}
const loadedItalicFamilies = new Set<string>()
export function isLabelEditorFontItalicLoaded(family: string): boolean {
return loadedItalicFamilies.has(family)
}
export async function ensureLabelEditorFontFamilyLoaded(family: string): Promise<void> {
const normalized = normalizeLabelEditorFontFamily(family) ?? LABEL_EDITOR_FONT_FAMILY
const pkg = fontPackageByFamily(normalized)
if (!pkg) return
await Promise.all(
pkg.variants.map((variant) => loadFontFaceOnce(pkg.family, pkg.dir, variant)),
)
}
export function collectFontFamiliesFromTemplate(template: SystemLabelTemplate): string[] {
const set = new Set<string>([LABEL_EDITOR_FONT_FAMILY])
for (const el of template.elements || []) {
const cfg = (el.config || {}) as Record<string, unknown>
const family = normalizeLabelEditorFontFamily(cfg.fontFamily ?? cfg.FontFamily)
if (family) set.add(family)
}
return [...set]
}
/** 预览 / 打印 canvas 绘制前调用,加载模板用到的字体 */
export async function ensureLabelEditorFontsForTemplate(
template: SystemLabelTemplate,
): Promise<void> {
const families = collectFontFamiliesFromTemplate(template)
await Promise.all(families.map((family) => ensureLabelEditorFontFamilyLoaded(family)))
}
/** App 启动后后台预加载全部打包字体,减少首次预览等待 */
export function preloadAllLabelEditorFonts(): Promise<void> {
if (!preloadAllPromise) {
preloadAllPromise = Promise.all(
LABEL_EDITOR_FONT_PACKAGES.map((pkg) => ensureLabelEditorFontFamilyLoaded(pkg.family)),
).then(() => undefined)
}
return preloadAllPromise
}