服务端 Canvas
在服务端绘制位图与 SVG,解码图片、排版文字和生成静态数据表,并用资源预算约束原生内存。
@pluxel/canvas 基于 @napi-rs/canvas 提供服务端绘图能力,包括位图 Canvas、SVG Canvas、图片解码、Pretext 文字布局和静态数据表。宿主会在分配原生内存前检查资源预算,避免单个 Plugin 无限制占用内存。
CanvasPlugin 必须依赖 FontsPlugin,也为 EChartsPlugin 提供底层绘图能力。业务 Plugin 直接绘图时只需注入 CanvasPlugin。
安装与 catalog
以下命令在快速开始生成的工作区根目录执行;按 添加插件 选择直接使用依赖的包,再运行 pnpm install。
pnpm catalog:add -- @pluxel/canvas @pluxel/fontshost catalog 至少包含 FontsPlugin、CanvasPlugin 和 consumer。CanvasPlugin required-depend FontsPlugin;业务 Plugin 只需注入 Canvas:
import { , } from '@pluxel/canvas'
import { , } from '@pluxel/runtime'
@()
export class extends {
constructor(private readonly : ) {
super()
}
async (): <> {
const = this..createCanvasSync(640, 320)
const = .getContext('2d')
.fillStyle = '#111827'
.fillRect(0, 0, .width, .height)
.font = `48px ${this..defaultFont.cssFamily}`
.fillStyle = '#fff'
.fillText('Pluxel', 48, 180)
.stroke(new ('M48 205 H250'))
return .encode('png')
}
}以下 host 是 测试宿主,用于验证装配。应用入口按 添加插件 配置清单、配置记录和自动启动。
import { CanvasPlugin } from '@pluxel/canvas'
import { FontsPlugin } from '@pluxel/fonts'
import { BadgePlugin } from '@acme/badge'
await host.start(BadgePlugin, {
catalog: [FontsPlugin, CanvasPlugin],
})调用 BadgePlugin.render() 后将返回的 Buffer 保存为 PNG,应得到带 Pluxel 字样的 640 × 320 图片。通过 HTTP 返回时设置 Content-Type: image/png;保存文件与响应处理都由业务插件决定。
createCanvasSync() 返回上游 native Canvas。名称明确表示 allocation、2D context、绘图和 measure 会占用调用线程;
encode/stream 遵循上游契约,surface 由 caller 持有。重 drawing 应进入业务 worker task。
创建 raster 与 SVG
const raster = this.canvas.createCanvasSync(1200, 630)
const svg = this.canvas.createSvgCanvasSync(1200, 630, {
mode: 'text-to-paths',
})
const svgBytes = svg.getContent()createSvgCanvasSync(width, height, options) 支持三个互斥 mode:
compact:默认,输出紧凑 SVG。text-to-paths:将文字转成 path,适合目标环境缺少字体时使用。relative-paths:使用相对 path encoding。
两种 factory 都会先检查 width、height 与总像素,并为新 context 设置当前 FontsPlugin.defaultFont。后续更改默认字体不会改写已有 context。
assertDimensions(width, height) 可在分配前复用相同校验。注意:factory 返回原生 Canvas,调用方之后仍能自行 resize;预算只保证通过 factory 发生的初始分配。
图片解码
const response = await fetchFromAnApprovedSource()
const bytes = new Uint8Array(await response.arrayBuffer())
const image = await this.canvas.decodeImage(bytes, { signal })
const surface = this.canvas.createCanvasSync(image.width, image.height)
surface.getContext('2d').drawImage(image, 0, 0)decodeImage() 只接受已经取得的非空 Uint8Array。它会检查 encoded bytes,解码后再检查图片 width、height 与像素数。网络认证、重试、redirect、origin policy 和下载上限应由业务 HTTP capability 负责;Canvas 不接受 URL,也不主动 fetch。
默认 dataOwnership: 'borrowed',调用方在 Promise settle 前不得修改 bytes;Canvas 在 native decode 前以 cooperative
chunk 复制输入。只有 render-local buffer 确定不再使用时才可传:
const image = await this.canvas.decodeImage(bytes, {
dataOwnership: 'owned',
signal,
})owned 会永久移交输入 storage,避免 copy;调用后不得再次读取或修改,即使 decode abort 或失败也不会返还。native decoder 无法取消已经提交的工作,signal 只会停止等待并丢弃迟到结果。
CanvasPlugin root 不提供裸 Image placeholder factory;native src setter 无法拦截,会绕过 decode budget。图片输入使用
decodeImage()。
文字准备与 Pretext
Canvas 将 @chenglou/pretext 的文字准备连接到 native measurement context:
import { layoutWithLines } from '@pluxel/canvas/pretext'
const prepared = this.canvas.prepareTextSync({
text: 'A progressively wrapped paragraph',
fontSize: 18,
whiteSpace: 'normal',
wordBreak: 'normal',
letterSpacing: 0,
})
const layout = layoutWithLines(prepared, 360, 24)主 API 包括:
prepareTextSync():准备普通文本。prepareTextWithSegmentsSync():保留手动 Canvas line rendering 所需的 segment 信息。prepareRichInlineSync():准备带独立 font、spacing、break 与 extra width 的 inline items。
省略 font 时使用当前 Pluxel 默认 family,可通过 fontSize 指定字号,默认 16px。传完整 Canvas font shorthand 时不能再传 fontSize。准备结果是不可变快照;默认字体之后发生变化,不会重写旧结果。
@pluxel/canvas/pretext 导出 Pretext 的纯布局 helper,包括 layoutWithLines()、layoutNextLine()、measureLineStats()、measureNaturalWidth()、line range walkers,以及对应 rich-inline helper。准备步骤读取 native 字体;之后的布局计算是纯 arithmetic。
共享 measurement cache 会在 Fonts revision 改变或累计准备字符达到 maxTextCacheCharacters 时清空。
生成静态数据表
当输出是报告、榜单或审计表格时,使用 @pluxel/canvas/table。它只处理 Canvas 最后一公里的列宽、单元格、Pretext 断行和默认省略;排序、筛选、数值本地化、二维码、图表和文件输出仍应由调用方或各自成熟工具负责。
import { drawTable, layoutTable } from '@pluxel/canvas/table'
const table = layoutTable({
width: 640,
columns: [
{ header: 'Service', weight: 2 },
{ header: 'Status', width: 112, textAlign: 'center' },
{ header: 'P95', width: 88, textAlign: 'right' },
],
rows: [
['API gateway', 'Healthy', '18 ms'],
['Background worker', 'Delayed', '81 ms'],
],
body: {
font: '14px ' + this.canvas.defaultFont.cssFamily,
lineHeight: 20,
padding: 6,
},
header: {
font: '700 14px ' + this.canvas.defaultFont.cssFamily,
background: '#e2e8f0',
},
prepareText: (input) => this.canvas.prepareTextWithSegmentsSync(input),
})
const height = Math.ceil(table.bounds.height)
this.canvas.assertDimensions(640, height)
const surface = this.canvas.createCanvasSync(640, height)
drawTable(surface.getContext('2d'), table)没有 width 的列按 weight(默认 1)分走余宽;若所有列都是 fixed,它们必须恰好占满总宽。每个 row 的 cell 数必须等于 columns 数。body.font 和 body.lineHeight 是必填项,确保 Pretext 测量与 native fillText() 使用同一 font。默认 maxLines: 1、overflow: 'ellipsis';设置 maxLines 或 overflow: 'clip' 可改为多行或直接裁切。
layoutTable() 返回不可变 snapshot,不创建 Canvas 或编码数据。调用方先取得实际高度,再用 assertDimensions() / factory 执行既有 host budget;过长报表应由调用方按页拆分,而不是让 table 工具暗中分配超大 surface。它同样可在 worker 中使用:将 prepareText 换成 createCanvasWorkerTextLayout(snapshot).prepareTextWithSegments,再向 worker adapter 创建的 context 调用 drawTable()。
可选 drawTable(context, table, { paintCell }) callback 在每个 cell 的 background 后、默认文本前执行;返回 'skip-text' 可完全接管该 cell 的内容。每次 callback 后都会恢复 Canvas state,避免影响其他 cell;callback 只属于本次 draw pass,不注册 Plugin、事件或持久化资源。
在 worker 中使用
native Canvas、Image 和 Plugin Context 不能 structured clone。主线程只把纯数据策略传给 worker:
const snapshot = this.canvas.workerSnapshotworkerSnapshot 包含:
limits:width、height、pixels 和 image bytes。textLimits:text length、rich item count 与 cache budget。decodeLimits:单个 worker adapter 的 native decode concurrency 与 waiting queue。font:默认 CSS family、Fonts revision,以及非 generic family 的存在要求。
worker artifact 从独立子入口重建 adapter,不启动第二个 CanvasPlugin:
import { createCanvasWorkerAdapter } from '@pluxel/canvas/worker'
export default async ({ canvas: snapshot }: Input) => {
const canvas = createCanvasWorkerAdapter(snapshot)
try {
const surface = canvas.createCanvas(640, 320)
return await surface.encode('png')
} finally {
await canvas.close()
}
}adapter 提供 createCanvas()、createSvgCanvas()、createImage()、decodeImageInto() 与 decodeImage()。
createImage() 只满足 ECharts 一类必须同步返回 placeholder 的受信任 platform contract;随后应把同一个 placeholder 与
bytes 交给 decodeImageInto(),它只执行一次 native decode,并在 Promise settle 前独占该 placeholder。普通调用方直接用
decodeImage()。直接写 native src 无法受 budget 约束。allocation/decode 会重新执行 host budget,decode concurrency/
queue 来自 snapshot;具体 family 若在线程 native registry 中不存在,会以 FONT_UNAVAILABLE 失败。adapter 拥有自己的
decode scheduler;任务结束时必须 await close()。close 会拒绝 queued decode、等待 already-submitted native decode
真正 settle,且不会回收已经返回给 caller 的 Canvas/Image/SVG。
只有 worker 需要 Pretext 时再引入额外子入口:
import { createCanvasWorkerTextLayout } from '@pluxel/canvas/worker/pretext'
const text = createCanvasWorkerTextLayout(snapshot)
const prepared = text.prepareText({ text: 'Hello', fontSize: 24 })@pluxel/canvas/worker/pretext 只提供受限 preparation;纯 layout/walker 始终从 @pluxel/canvas/pretext 导入。这避免 ECharts 等不使用 Pretext 的 worker artifact 承担其代码与 cache 成本。两个 worker 子入口都不提供 Plugin、Context、Workbench 或字体 mutation。
配置与职责
await host.start(CanvasPlugin, {
catalog: [FontsPlugin],
initialConfig: {
maxWidth: 8192,
maxHeight: 8192,
maxPixels: 16_777_216,
maxImageBytes: 32 * 1024 * 1024,
maxConcurrentDecodes: 2,
maxQueuedDecodes: 32,
maxQueuedDecodesPerConsumer: 8,
maxConcurrentDecodesPerWorkerAdapter: 1,
maxQueuedDecodesPerWorkerAdapter: 32,
maxTextCharacters: 100_000,
maxRichTextItems: 2_048,
maxTextCacheCharacters: 1_000_000,
},
})这里的 host 是 createRuntimeTestHost() fixture;initialConfig 只用于首次 lifecycle。后续更新使用
host.config.patch(),production deployment 则通过自己的 ConfigService 管理相同 record。
| 字段 | 默认值 | 检查对象 |
|---|---|---|
maxWidth | 8192 | factory 分配和 decoded image 的宽度 |
maxHeight | 8192 | factory 分配和 decoded image 的高度 |
maxPixels | 16,777,216 | width × height;默认相当于 64 MiB raw RGBA |
maxImageBytes | 32 MiB | decodeImage() 接受的 encoded bytes |
maxConcurrentDecodes | 2 | CanvasPlugin root 的在途 decode 数 |
maxQueuedDecodes | 32 | CanvasPlugin root 的合计等待数 |
maxQueuedDecodesPerConsumer | 8 | 单个 caller 等待的 root decode 数 |
maxConcurrentDecodesPerWorkerAdapter | 1 | 每个 detached worker adapter 的在途 decode 数 |
maxQueuedDecodesPerWorkerAdapter | 32 | 每个 detached worker adapter 的等待数 |
maxTextCharacters | 100,000 | 单次 Pretext preparation 的 UTF-16 长度 |
maxRichTextItems | 2,048 | 单次 rich-inline preparation 的 item 数 |
maxTextCacheCharacters | 1,000,000 | 清空共享 measurement cache 前的累计字符预算 |
CanvasConfig 分开配置 Canvas root 与每个 worker adapter 的 native decode admission,不配置通用 worker task queue、render timeout、 output bytes、DPR 或主题:
- Worker task 的线程并发与队列由 runtime 的 root-owned
ctx.workers配置。 maxConcurrentDecodesPerWorkerAdapter是每个 adapter 的局部上限;ECharts 每 job 创建一个 adapter,默认 4 个 Runtime workers × 1 个 decode,仍不是可接管 libuv 的进程级线程池。提高它会按 active worker 数产生乘法,应与UV_THREADPOOL_SIZE、其他 native work 和 RSS 基准一起调整。- encoded output 的格式与大小由调用方和上游 encoder 决定。
- 图表 DPR、主题与 data URL 上限由 EChartsPlugin 配置。
字体、Workbench 与生命周期
CanvasPlugin 使用 FontsPlugin 的默认字体,并在自己的详情页挂载统一字体选择器。字体上传、删除和持久化只属于 FontsPlugin;Workbench disabled 不影响字体恢复、Canvas 创建、解码或 worker snapshot。
Canvas 为每个 caller generation 建立 lease。consumer stop/replacement 会中止该 generation 正在等待的 decode;已返回给 caller 的 native surface 仍由 caller 自己持有,不应跨 generation 保存。
错误处理
CanvasError.code 提供稳定分类:
- lifecycle:
NOT_RUNNING。 - surface budget:
INVALID_DIMENSIONS、DIMENSIONS_EXCEEDED、PIXELS_EXCEEDED。 - image:
INVALID_IMAGE、IMAGE_BYTES_EXCEEDED、DECODE_BUSY。 - text:
INVALID_TEXT、TEXT_TOO_LARGE、TEXT_LAYOUT_UNAVAILABLE。 - worker/font:
FONT_UNAVAILABLE、INVALID_WORKER_SNAPSHOT。
不要在 consumer 中捕获后放宽 host budget;应缩小输入、拒绝任务,或由 host operator 明确调整配置。
最后更新于