服务端字体
发现、注册和管理服务端字体,并为 Canvas、ECharts 与 Takumi 提供统一字体资源。
@pluxel/plugin-fonts 统一管理服务端字体:发现系统字体、注册 Plugin 随包携带的字体、保存从 Workbench 上传的字体,
并为 Canvas/ECharts 选择 native default、为 Takumi 等独立 renderer 提供可移植 bytes。
Canvas/ECharts 能使用系统字体;Takumi 需要 Fonts 管理的可移植字体字节。需要一致的跨机器输出时,通过部署配置提供字体文件、随包提供字体或从 Workbench 上传;只安装 Fonts 不会替操作系统安装字体。
系统发现、部署字体与程序化注册不要求额外 Host 服务。上传字体与保存默认偏好才需要 Persistence;Workbench 是可选编辑入口。
DefaultFontSnapshot、FontFamilySnapshot 和 FontStyleSnapshot 从包根以 type 导入;它们只描述字体数据。Workbench definition 与 RPC API 从 /workbench 导入。
无 Workbench 部署
FontsConfig 是插件使用的同一 schema,可由 Host config、envBinding 或 fileBinding 提供。应用在 envBindings 中声明:
import { envBinding } from '@pluxel/host'
import { FontsPlugin, FontsConfig } from '@pluxel/plugin-fonts'
envBinding(FontsPlugin, {
config: {
schema: FontsConfig,
mapping: { files: 'FONT_FILES', defaultFamily: 'FONT_FAMILY' },
},
})例如 FONT_FILES='["/app/fonts/ReportSans.ttf","/app/fonts/ReportSans-Bold.ttf"]',FONT_FAMILY='Report Sans'。files 使用绝对路径 JSON 数组;启动时按配置顺序读取并注册,重复路径只读一次。部署文件属于 provider generation,停止时释放;不写入上传集合。文件缺失、非法字体或超限使启动失败,并释放已经注册的字体。
配置字体不设置 Canvas alias,使用内嵌 family,因而同一字节可供 Typst、Takumi 等独立 renderer 使用。单文件和 provider 总预算仍生效,不占 consumer registration 或 managed upload 配额。配置变化下次 provider restart 生效;环境与文件绑定的变更需要重新创建 Host,来源优先级由 Host 配置维护。
何时直接使用 FontsPlugin
- Plugin 自带
.ttf、.otf、.woff或.woff2文件,需要在服务端 renderer 中注册。 - 需要读取当前可用的字体 family 或 provider 默认字体。
- 自己实现 renderer,需要用
revision使文字测量缓存失效。 - renderer 有自己的字体 registry,需要按
portableFonts.revision读取 managed/programmatic bytes。 - 需要在 Plugin 的 Workbench 页面嵌入统一的字体选择器。
只使用 Canvas、ECharts 或 Takumi 的业务 Plugin 通常不必直接注入 Fonts;由对应 renderer 依赖它即可。
安装与 catalog
以下命令在快速开始生成的工作区根目录执行;按 添加插件 选择直接使用依赖的包,再运行 vp install。
vp exec pluxel pncat add @pluxel/plugin-fontshost catalog 必须包含 FontsPlugin。直接使用字体能力的 Plugin 将它声明为 required dependency:
import { } from '@pluxel/plugin-fonts'
import { , } from '@pluxel/core'
@()
export class extends {
constructor(private readonly : ) {
super()
}
}以下 host 是 测试宿主,用于验证装配。应用入口按 添加插件 配置清单、配置记录和自动启动。
import { FontsPlugin } from '@pluxel/plugin-fonts'
import { ReportsPlugin } from '@acme/reports'
await host.start(ReportsPlugin, { catalog: [FontsPlugin] })系统字体由 @napi-rs/canvas 的 platform font manager 在 FontsPlugin 启动时发现。它不会安装或删除操作系统字体,也不会把字体文件发到浏览器。
注册 Plugin 随包携带的字体
使用绝对服务端路径注册静态资源:
import { } from '@pluxel/plugin-fonts'
import { , } from '@pluxel/core'
import { } from 'node:url'
@()
export class extends {
constructor(private readonly : ) {
super()
}
protected override async () {
await this..({
: (new ('../assets/ReportSans.woff2', import..)),
: 'Report Sans',
})
}
}注册后检查 this.fonts.families 是否包含 Report Sans,再让 renderer 使用它生成一张含中文、数字和标点的图片。部署产物也必须包含 assets/ReportSans.woff2,路径相对于最终模块位置解析。
registerFromPath() 只接受绝对路径,异步读取并检查 maxFontBytes;读取期间文件尺寸变化会被拒绝。
family 是可选 alias;省略时使用字体内嵌的 family metadata。
已经取得字节时使用 register():
const registration = await this.fonts.register({
data: fontBytes,
family: 'Report Sans',
signal,
})
console.log(registration.families)
registration.dispose()data 必须是非空 Uint8Array,在 Promise settle 前保持不变。读取、复制与 hash 可取消;
最终 native 注册是受 byte 上限约束的同步操作,不能被抢占。返回的
FontRegistration 包含:
families:本次注册新增或改变的 family。active:registration 是否仍有效。dispose():提前移除注册;可重复调用。
注册归属于当前 caller generation。consumer 停止或被 replacement 时,即使没有手动 dispose(),FontsPlugin 也会移除对应 native FontKey。因此不要将 registration handle 跨 generation 缓存。
读取字体与默认选择
const families = this.fonts.families
const current = this.fonts.defaultFont
const revision = this.fonts.revisionfamilies 是 detached、只读的 family snapshot。每项包含:
family:字体 family 名称。source:system或registered。styles:可用的 weight、width 与 style。
defaultFont 包含 family、可安全放进 Canvas font shorthand 的 cssFamily,以及选择来源。解析优先级为:
- 显式
defaultFamilyhost config。 - provider-wide 持久化 preference。
- 当前平台的自动系统字体。
- generic
sans-serif。
source 表示是哪一层选中了默认值:preference、config、system 或 generic,不是字体资源的来源。preferredFamily 或 configuredFamily 可能存在但暂时不可用,此时解析会继续 fallback。preference 是 Fonts 自身的 provider-wide domain state;显式配置暂时遮盖它,取消配置后可重新生效。即使 Workbench 未启用,安装 Persistence 后仍会恢复保存状态。
revision 是进程内字体注册与默认选择的变更信号。renderer 应把它纳入文字测量 cache key,或在其变化时清空缓存。直接操作 GlobalFonts 不会遵守这一契约。
独立 native renderer 不能共享 Canvas GlobalFonts 时,使用轻量 metadata snapshot 与按需 byte read:
const snapshot = this.fonts.portableFonts
for (const font of snapshot.fonts) {
const detachedBytes = await this.fonts.readPortableFont(font.id, { signal })
// 注册进 renderer-local registry;按 snapshot.revision 复用结果。
}portableFonts 包含配置 files、managed uploads 和 register() / registerFromPath() 资源。metadata snapshot 会缓存,
不因轮询复制 font bytes;readPortableFont(id, { signal }) 才 cooperative 返回 detached Uint8Array。相同 bytes + family alias 使用同一
content ID,最后一个 registration 释放后才从集合移除。平台自动发现的 system font 没有 FontsPlugin-owned 文件,
因此诚实地不进入可移植集合。
Workbench 管理与 Selection Attachment
FontsPlugin 自己的 manager View 管理 provider-owned 字体集合:上传、删除字体并设置默认 family。上传字体持久化在
host persistence 中,provider 重启时恢复。上传受 maxFontBytes / maxManagedFonts 约束;
关闭页面会阻止文件读取完成后的 RPC,但不能取消浏览器已经开始的 File.arrayBuffer()。
其他 Plugin 不应复制上传管理界面。如果只需在自己的详情页让用户选择统一默认字体,放置 Fonts 提供的 Attachment:
import { FontsWorkbench } from '@pluxel/plugin-fonts/workbench'
import { workbench } from '@pluxel/workbench'
export const ReportsWorkbench = workbench.define({
fonts: FontsWorkbench.selection.place(workbench.tab({ label: 'Fonts' })),
})
protected override init() {
this.ctx.workbench?.publish(ReportsWorkbench, {
fonts: { provider: this.fonts },
})
}FontsWorkbench.selection 提供 FontSelectionApi.snapshotDto() 与
setPreferredFamily(family | null);传 null 恢复 host config / 自动选择。selection 修改 provider 统一默认值,
不是 consumer 私有偏好。Consumer 不创建转发 target;Canvas、ECharts、Takumi 已放置该 selector,通常无需重复添加。
Workbench disabled 只会关闭界面,不会阻止 managed fonts 恢复、程序化注册或 headless 渲染。缺少 Persistence 时字体发现、配置文件与程序化注册仍可用;setPreferredFamily() 和 managed 上传/删除报 PERSISTENCE_REQUIRED,不会假装保存成功或暗中建立存储。
配置
在应用的 Plugin config 中按需覆盖以下默认值:
| 字段 | 默认值 | 职责 |
|---|---|---|
files | [] | provider 启动加载的绝对字体文件路径 |
defaultFamily | 自动选择 | 优先于保存偏好的部署默认 family |
maxRegistrationsPerConsumer | 32 | 一个 caller 同时持有的程序化 registration 上限 |
maxNativeRegistrations | 512 | 此 FontsPlugin node 持有的 native key 总上限 |
maxTotalFontBytes | 256 MiB | active native registrations 的合计 bytes |
maxConcurrentFontTasks | 4 | 同时进行的 caller copy/read/hash 数 |
maxQueuedFontTasks | 32 | 所有 caller 合计等待的字体任务数 |
maxQueuedFontTasksPerConsumer | 8 | 单个 caller 等待的字体任务数 |
maxPendingManagedTasks | 32 | 已接纳的串行 Workbench 操作数(含 active) |
maxManagedFonts | 64 | provider-owned 持久化集合的字体数上限 |
maxFontBytes | 16 MiB | 单个注册或上传字体文件的字节上限 |
系统字体不计入 maxManagedFonts,也不会被 FontsPlugin cleanup。尺寸、像素、图片解码和 DPR 都不属于 Fonts 配置:它们分别由 Canvas 与 ECharts 负责。
错误处理
字体操作失败时抛出 FontsError,可按 error.code 分类:
NOT_RUNNING:provider 或 caller generation 已停止。INVALID_INPUT:路径、字节、文件名或 family alias 无效。INVALID_FONT:native registry 拒绝字体。FONT_TOO_LARGE、FONT_LIMIT_EXCEEDED:触发 byte/count 配额。FONT_BUSY:全局或 caller 字体任务队列已满。FONT_NOT_FOUND:选择或删除的字体不存在。CORRUPT_FONT_STORAGE:持久化 managed font 或默认选择损坏。PERSISTENCE_REQUIRED:当前 Host 未安装保存偏好和上传集合所需的 Persistence。
不要依赖错误 message 做分支;message 用于诊断,稳定分类在 code。
默认字体变化只影响之后创建的 Canvas context 和之后执行的 ECharts/Takumi render。已经准备好的文字布局与已有 native context/renderer state 保持不变;portable 集合变化会让 Takumi 在新 revision 创建新的 registry。
将可恢复失败交给业务调用方
业务品牌字体注册允许用户更换文件时,可只将内容不合法和单个文件超限转成 Result。
下面的 fonts 是 consumer 注入的 FontsPlugin,并未另建字体管理入口。
import { , } from '@pluxel/core/better-result'
import {
,
type ,
type FontRegistration,
type ,
} from '@pluxel/plugin-fonts'
export class extends ('FontRejected')<{
: 'invalid_font' | 'too_large'
}> {}
export async function (
: ,
: ,
): <<FontRegistration, >> {
try {
return .(await .())
} catch () {
if ( instanceof && . === 'INVALID_FONT') {
return .(new ({ : 'invalid_font' }))
}
if ( instanceof && . === 'FONT_TOO_LARGE') {
return .(new ({ : 'too_large' }))
}
throw
}
}Err 的 reason 决定提示换文件还是缩小文件。Ok 的 value 仍是 FontRegistration:caller generation
停止时自动释放,也可调用 dispose() 提前释放。Result 不负责释放资源,registration 不能变成 DTO。
调用参数错误、累计容量、存储损坏、busy、取消和停止保持原异常;启动恢复失败仍必须让 Plugin 启动失败。
最后更新于