@composy/plate-editor-core
v0.0.4
Published
Framework-neutral license plate replacement engine
Downloads
38
Maintainers
Readme
@composy/plate-editor-core
@composy/plate-editor-core 是一个框架无关的浏览器车牌图片编辑引擎。它提供两层能力:
Editor:只负责 Canvas 编辑、图片加载、检测器、车牌图层、透视渲染、预设、历史记录、状态序列化和导出;适合原生 DOM、React、Vue、Web Components 或自行编写 UI。PlateEditorStudio:在Editor之上提供完整的原生 DOM 工作台,包括工具栏、导入、识别、矩形框选、四角点选、预设弹窗、缩放、撤销/重做、原图对比、主题和保存。
所有图片编辑在浏览器 Canvas 中完成。ONNX 推理默认也在浏览器内执行;远程大模型检测是显式调用的可选能力,不会自动上传图片。
车牌检测模型、模型权重、模型输出格式和模型训练数据不由本包提供。生产环境应使用适合目标国家、车牌类型、拍摄角度和光照条件的模型,并让用户确认自动检测结果。
目录
- 安装与运行环境
- 产物与导入
- 五分钟上手:Editor
- 使用完整 Studio
- Studio 模式、触发器与弹窗
- Studio 工具栏定制
- 全部构造参数
- 图片来源与限制
- 检测器与模型
- ONNX 参数完整说明
- 远程大模型检测
- 自定义检测器
- 车牌图层与样式
- 文字、图片和混合内容
- 预设
- 视口、历史和状态
- 导出、下载和保存
- 全部事件
- 全部实例 API
- 根入口导出
- UMD、示例与构建
- 销毁与资源管理
安装与运行环境
pnpm add @composy/plate-editor-core使用内置 ONNX 检测器时安装 onnxruntime-web:
pnpm add onnxruntime-web如果只使用手动框选、自己实现的 PlateDetector 或远程检测器,可以不单独安装 onnxruntime-web。包本身仍然是浏览器库,需要以下 Web API:
document、HTMLCanvasElement、Image;URL.createObjectURL、Blob、File;- 使用
ImageBitmap、WebGPU、WASM 或远程检测时,需要对应浏览器能力; - 服务端 Node.js 环境不能直接创建
Editor,除非提供完整的 DOM、Canvas 和图片运行时。
容器必须有可计算的宽度和高度。对于 Editor,建议显式设置容器高度:
<div id="plate-editor" style="width: 100%; height: 640px"></div>产物与导入
普通项目推荐使用包根入口,不要手动拼接 es/、lib/ 路径:
| 入口 | 内容 | 适用场景 |
| --- | --- | --- |
| @composy/plate-editor-core | ESM/CJS 条件入口 | Vite、Webpack、Rollup、Node bundler |
| @composy/plate-editor-core/esm | 明确的 ESM 入口 | 构建工具要求显式 ESM 时 |
| @composy/plate-editor-core/esm/* | ESM preserve-modules 子模块 | 需要指定模块时 |
| dist/index.js | 未压缩 UMD | 浏览器调试 |
| dist/index.min.js | 压缩 UMD | CDN、静态页面生产环境 |
import {
Editor,
PlateEditorStudio,
BUILT_IN_PLATE_PRESETS,
} from '@composy/plate-editor-core'
import type {
Plate,
PlateEditorEvents,
PlateEditorOptions,
PlateExportOptions,
} from '@composy/plate-editor-core'CommonJS:
const { Editor, PlateEditorStudio } = require('@composy/plate-editor-core')CommonJS 入口仍然需要浏览器 DOM,不能直接当作 Node 服务端图片处理模块。
五分钟上手:Editor
Editor 默认使用手动检测器,不会把任意高对比度区域自动当成车牌。最小手动编辑示例:
<div id="plate-editor" style="height: 640px"></div>
<input id="image-input" type="file" accept="image/jpeg,image/png,image/webp" />
<button id="export-button" type="button">导出</button>import { Editor } from '@composy/plate-editor-core'
const editor = new Editor({
container: '#plate-editor',
autoDetect: false,
maxPlates: 1,
})
document.querySelector<HTMLInputElement>('#image-input')!.addEventListener('change', async event => {
const file = (event.target as HTMLInputElement).files?.[0]
if (!file) return
await editor.loadImage(file)
editor.setAddMode(true)
})
document.querySelector('#export-button')!.addEventListener('click', async () => {
const blob = await editor.export({
type: 'image/png',
useOriginalResolution: true,
})
// 上传 blob、创建 object URL 或交给业务存储。
console.log(blob.type, blob.size)
})
window.addEventListener('beforeunload', () => { void editor.destroy() })也可以在创建时传入图片:
const editor = new Editor({
container: document.querySelector('#plate-editor')!,
image: '/images/car.jpg',
autoDetect: false,
})options.image 的异步加载失败会通过 image:error 事件通知;构造函数启动的异步加载不会把错误重新抛到构造调用栈中。需要明确控制错误时,推荐创建后调用 loadImage()。
使用完整 Studio
import {
BUILT_IN_PLATE_PRESETS,
PlateEditorStudio,
} from '@composy/plate-editor-core'
const studio = new PlateEditorStudio({
container: '#plate-editor',
mode: 'inline',
autoDetect: false,
showBuiltInPresets: true,
presets: BUILT_IN_PLATE_PRESETS,
defaultPresetId: 'cn-blue-standard',
theme: 'auto',
primaryColor: '#177fd3',
})
await studio.loadImage(file)
console.log(studio.editor.platesList)
await studio.save('vehicle-plate', { type: 'image/png' })
await studio.destroy()Studio 会自动注入 Core 的 Canvas 样式和 Studio 样式,并把底层 Editor 暴露在 studio.editor。复杂业务可以直接使用 studio.editor 的全部 API。
Studio 模式、触发器与弹窗
inline 与 dialog
| mode | 行为 | 适用场景 |
| --- | --- | --- |
| 'inline'(默认) | 直接显示编辑工作区和工具栏;保存按钮位于工具栏右侧 | 固定编辑区域、详情页、后台页面 |
| 'dialog' | 初始只显示启动按钮;选择或加载图片后打开全屏编辑对话框;保存按钮位于对话框底部 | 表单字段、车辆图片选择器、按需编辑 |
const studio = new PlateEditorStudio({
container: '#dialog-editor',
mode: 'dialog',
autoDetect: false,
})
// 业务调用会加载图片并自动打开编辑器对话框。
await studio.loadImage(file)
// 关闭对话框只隐藏界面,不清除当前图片、图层或历史状态。自定义触发元素
trigger 必须是创建 Studio 前已经存在的 HTMLElement 或 CSS 选择器,并且不应放在 container 内:
<button id="choose-vehicle" type="button">选择车辆图片</button>
<div id="dialog-editor"></div>const studio = new PlateEditorStudio({
container: '#dialog-editor',
mode: 'dialog',
trigger: '#choose-vehicle',
})
// 不需要模拟按钮点击,也可以主动打开系统文件选择器。
studio.openPicker()如果外部触发元素被销毁后重新创建,需要销毁并重新创建 Studio,重新绑定新的 DOM 元素。
弹窗挂载与层级
Studio 的预设弹窗和编辑器弹窗会挂载到 document.body 下的独立 portal,而不是留在业务容器内部。这样可以避开宿主页面的 overflow、transform、isolation 和局部 stacking context;弹窗使用高层级固定定位。组件销毁时会移除 portal。
这意味着业务页面不需要额外为弹窗配置 z-index。如果业务自身有全局层级规范,应确保其最高层级没有使用浏览器允许的最大值附近,并避免用全局规则覆盖 .ldesign-plate-editor-studio__portal。
Studio 的两个 Studio 事件:
studio.on('dialog:open', ({ type }) => {
console.log(type) // 'editor' 或 'presets'
})
studio.on('dialog:close', ({ type }) => {
console.log(type)
})Studio 工具栏定制
工具栏按钮 ID:
upload、detect、manual、manual-corners、pan、presets、zoom-out、zoom-in、fit、undo、redo、compare、compression、download、theme、save。
Studio 会把 manual 与 manual-corners 合并为一个互斥的“添加方式”按钮选择器。点击当前方式后,会从工具栏上方向上滑出带图标的选项面板;用户只能在“矩形框选”和“四角点选”之间选择一种当前新增方式。这两个 ID 仍可分别用于配置选项的文字、图标和可见性。矩形框选完成后,车牌会以选区中心为基准略微外扩,便于覆盖原车牌;四角点选则严格使用用户点击的四个角。
const studio = new PlateEditorStudio({
container: '#plate-editor',
toolbar: {
buttons: {
upload: { label: '选择车辆图片', icon: 'image-plus' },
detect: { label: '重新识别车牌', icon: 'scan-search' },
manual: {
label: '矩形框选区域',
icon: '<path d="M5 4h14v16H5z"/><path d="M8 8h8M8 12h5"/>',
},
'manual-corners': { label: '依次点击四个角点', icon: 'scan-line' },
pan: { label: '移动画布', icon: 'hand' },
compare: { visible: false },
theme: { label: '切换编辑器主题' },
},
},
})单个按钮配置:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| visible | boolean | false 隐藏按钮;不会删除其它工具栏布局 |
| label | string | 同时作为无障碍名称和悬浮提示 |
| icon | PlateEditorIconSource | 内置图标名、SVG body 或完整 <svg> |
| element | HTMLButtonElement | 使用业务创建的原生按钮替换默认按钮 |
| onClick | (event: MouseEvent) => void | 替换默认动作;存在时不会执行内置动作 |
| attributes | Record<string, string \| number \| boolean \| null \| undefined> | 写入生成按钮或业务按钮的原生 HTML 属性;false/null/undefined 会移除属性 |
内置图标名:image-up、image-plus、scan-search、scan-line、mouse-pointer-2、pointer、swatches、palette、minus、plus、maximize、undo、redo、compare、columns-2、compress、sliders-horizontal、download、arrow-right、sun、moon、close。
compression 是内置压缩质量菜单,按钮会显示滑杆图标和“压缩”文字,默认提供 100%、92%、80%、60%、40% 五档;它会作用于 Studio 的 export()、download()、save() 和工具栏保存动作。通过 toolbar.buttons.compression.visible = false 可隐藏。
替换保存按钮的例子:
const saveButton = document.createElement('button')
saveButton.type = 'button'
saveButton.textContent = '保存到业务系统'
let studio!: PlateEditorStudio
studio = new PlateEditorStudio({
container: '#plate-editor',
toolbar: {
buttons: {
save: {
element: saveButton,
onClick: () => void studio.save('vehicle-plate'),
},
},
},
})inline 保存按钮位于工具栏右侧;dialog 保存按钮位于弹窗底部右侧。两者都调用同一套 save() 逻辑。
全部构造参数
PlateEditorOptions:Editor 基础参数
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| container | HTMLElement \| string | 必填 | 编辑器挂载容器或 CSS 选择器;找不到时构造函数抛错 |
| image | PlateSource | 无 | 创建后自动加载的图片 |
| detector | PlateDetector | 手动检测器 | 自定义检测器;传入后优先于 model |
| model | ModelOptions | 无 | 配置 ONNX 检测器;只有存在 model.url 才会启用 |
| autoDetect | boolean | false | image 加载完成后是否自动检测;Studio 默认会改为 true。只有在配置了 ONNX 模型(model.url)或自定义 detector 时才会真正自动识别,否则跳过 |
| useHeuristicFallback | boolean | false | 没有 detector 和有效 model.url 时启用实验性启发式检测 |
| minimumDetectionConfidence | number | 0.45 | 低于该置信度的候选会被丢弃,并限制在 0..1 |
| maxPlates | number | 1 | 最大车牌图层数;总是至少为 1 |
| locale | 'zh-CN' \| 'en-US' | 无 | 预留的业务语言元数据;当前 Core Canvas 与 Studio 文案不依赖此字段 |
| readonly | boolean | false | 禁止拖拽、手动框选、检测和 Studio 编辑动作;仍可读取和导出 |
| historyLimit | number | 40 | 历史快照上限;总是至少保留 1 条 |
| maxFileSize | number | 30 * 1024 * 1024 | Blob/File 最大字节数 |
| maxImageDimension | number | 12000 | 图片宽或高最大像素数 |
| maxPixels | number | 80_000_000 | 图片最大像素总数 |
| crossOrigin | 'anonymous' \| 'use-credentials' \| null | 浏览器默认 | 加载跨域 URL 时设置 HTMLImageElement.crossOrigin |
| background | string | '#111820' | Canvas 容器背景色 |
| initialStyle | Partial<PlateStyle> | 内置默认样式 | 新图层的基础样式 |
| showBuiltInPresets | boolean | false | 是否加入包内五个车牌预设 |
| presets | readonly PlatePresetInput[] | [] | 业务预设;同 ID 的业务预设覆盖内置预设 |
| defaultPresetId | string | 无 | 新图层和 Studio 当前应用使用的默认预设 ID;不存在时忽略 |
| handleSize | number | 6 | 四角手柄直径,使用原图逻辑坐标 |
| edgeHandleSize | number | 6 | 四边手柄厚度,使用原图逻辑坐标 |
PlateEditorStudioOptions:Studio 额外参数
Studio 继承上表除 container、image、autoDetect 之外的所有参数,并新增:
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| container | HTMLElement \| string | 必填 | Studio 根节点挂载容器 |
| image | PlateSource | 无 | 创建后加载并按 autoDetect 检测 |
| autoDetect | boolean | true | 图片加载后自动检测;Studio 为了完整工作流默认开启 |
| theme | 'auto' \| 'light' \| 'dark' | 'auto' | Studio 表面主题;auto 跟随系统偏好 |
| mode | 'inline' \| 'dialog' | 'inline' | 直接显示或选择图片后弹窗显示 |
| trigger | HTMLElement \| string | 无 | dialog 模式复用已有入口 |
| toolbar | PlateEditorToolbarOptions | {} | 工具栏按钮配置 |
| saveHandler | PlateEditorSaveHandler | 无 | 接收已渲染 Blob;不调用 download() 就不会下载 |
| exportOptions | PlateExportOptions | {} | Studio 工具栏和无参数导出/保存使用的默认格式、质量、尺寸与底色 |
| primaryColor | string | 全局默认色 | Studio 工具栏强调色;支持十六进制颜色 |
图片来源与限制
PlateSource 支持:
type PlateSource =
| string // URL、data URL、blob URL
| Blob
| File
| ArrayBuffer
| HTMLImageElement
| ImageBitmapawait editor.loadImage('/images/car.jpg')
await editor.loadImage(file)
await editor.loadImage(new Blob([bytes], { type: 'image/png' }))
await editor.loadImage(await createImageBitmap(file))对 Blob/File,Core 会检查 MIME 类型是否为 JPEG、PNG 或 WebP,并检查文件大小。所有来源都会检查图片尺寸和总像素数。超限或加载失败时:
- 触发
image:error; loadImage()Promise reject;- Studio 会把错误显示在工作区提示区域。
跨域图片必须由服务器返回正确的 CORS 响应头,否则 Canvas 可能被 taint,最终 export() 会失败。crossOrigin 只设置浏览器加载策略,不能替服务器补充 CORS。
检测器与模型
检测器统一实现以下接口:
interface PlateDetector {
backend?: 'none' | 'heuristic' | 'webgpu' | 'wasm' | 'remote' | 'custom'
preload?: () => Promise<void>
detect: (
context: PlateDetectorContext,
options?: { precise?: boolean; maxResults?: number },
) => Promise<PlateDetection[]>
destroy?: () => void
}优先级:
options.detector;options.model.url创建的 ONNX 检测器;options.useHeuristicFallback === true创建的启发式检测器;- 其它情况使用不返回候选的手动检测器。
检测结果进入编辑器前还会经过置信度、四边形有效性、车牌长宽比和最大数量过滤。detectPlates({ replace: true }) 默认清除旧图层再写入结果;传 replace: false 会保留已有图层。
预置检测器工厂
| 工厂 | backend | 说明 |
| --- | --- | --- |
| createManualDetector() | 'none' | 永远返回空结果;适合完全手动模式 |
| createHeuristicDetector() | 'heuristic' | 基于颜色、边缘、比例和纹理的实验性本地检测;不能替代目标模型 |
| createOnnxPlateDetector(model) | 'webgpu' 或 'wasm' | 使用 ONNX Runtime 推理 |
| createLargeModelPlateDetector(options) | 'remote' | 调用兼容 Chat Completions 的远程视觉接口 |
setDetector() 会先预加载新检测器,成功后再销毁旧检测器;新检测器预加载失败时会恢复旧检测器。
ONNX 参数完整说明
const editor = new Editor({
container: '#plate-editor',
model: {
url: '/models/license-plate-detector.onnx',
runtime: ort,
preferredBackends: ['webgpu', 'wasm'],
inputWidth: 640,
inputHeight: 640,
inputLayout: 'nchw',
channelOrder: 'rgb',
normalization: 'zero-one',
outputFormat: 'corners',
coordinateFormat: 'normalized',
},
})| ModelOptions 字段 | 类型 | 默认值/行为 | 说明 |
| --- | --- | --- | --- |
| url | string | 无;ONNX 必需 | 模型 URL、同源路径或可访问的模型地址 |
| preferredBackends | ('webgpu' \| 'wasm')[] | ['webgpu', 'wasm'] | 按顺序尝试执行后端 |
| scoreThreshold | number | 0.45 | 模型候选初始置信度门槛 |
| nmsThreshold | number | 0.45 | ONNX 候选抑制重叠阈值 |
| inputWidth | number | 640 | 模型输入宽,最小按 32 处理 |
| inputHeight | number | 640 | 模型输入高,最小按 32 处理 |
| inputName | string | Session 第一个输入 | 指定输入 Tensor 名称 |
| outputName | string | 指定名、首个输出或第一个返回值 | 指定输出 Tensor 名称 |
| inputLayout | 'nchw' \| 'nhwc' | 'nchw' | 输入 Tensor 排布 |
| channelOrder | 'rgb' \| 'bgr' | 'rgb' | 通道顺序 |
| normalization | 'zero-one' \| 'minus-one-one' \| 'none' | 'zero-one' | 像素归一化方式 |
| outputFormat | 'auto' \| 'yolo' \| 'xyxy' \| 'corners' | 'auto' | 输出为 YOLO 中心框、xyxy 框或四角点 |
| coordinateFormat | 'auto' \| 'normalized' \| 'input' \| 'original' | 'auto' | 输出坐标相对于归一化空间、模型输入或原图 |
| padValue | number | 114 | letterbox 填充值,限制在 0..255 |
| verifyCandidates | boolean | true | 用轻量外观检查剔除车身部件、反光等候选 |
| appearanceThreshold | number | 精确模式约 0.32,普通模式约 0.36 | 外观检查最低分 |
| vehiclePositionPrior | boolean | true | 偏好车辆前后部常见车牌区域 |
| heuristicFallback | boolean | true | 模型没有可用候选时运行本地启发式补偿 |
| wasmPaths | string | ONNX Runtime 默认路径 | 设置 WASM 文件目录 |
| runtime | OnnxRuntimeLike | 全局 ort 或动态导入 | 显式注入 onnxruntime-web,推荐用于 bundler/CSP 环境 |
Vite 典型静态资源布局:
public/models/license-plate-detector.onnx
public/onnxruntime/ # 如果需要自定义 WASM 路径import * as ort from 'onnxruntime-web'
const editor = new Editor({
container: '#plate-editor',
model: {
url: '/models/license-plate-detector.onnx',
runtime: ort,
wasmPaths: '/onnxruntime/',
outputFormat: 'corners',
coordinateFormat: 'normalized',
preferredBackends: ['webgpu', 'wasm'],
},
})
await editor.preload()
await editor.loadImage(file)
const plates = await editor.detectPlates({ precise: true, replace: true })setModel() 会先加载新模型,成功后替换当前检测器;失败时旧检测器仍可继续使用。preload() 只预加载当前检测器,参数位置保留给兼容代码,当前不会用它动态替换模型。
远程大模型检测
远程检测是显式调用,不会因为配置了 LargeModelOptions 就自动上传图片。请求是 POST JSON,默认构造 OpenAI-compatible messages,图片作为 data URL 发送:
const plates = await editor.detectWithLargeModel({
endpoint: 'https://example.com/v1/chat/completions',
model: 'vision-model',
apiKey: runtimeToken,
coordinateFormat: 'normalized',
maxImageSide: 1600,
imageMimeType: 'image/jpeg',
imageQuality: 0.9,
timeout: 45_000,
maxTokens: 300,
temperature: 0,
}, { replace: true })| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| endpoint | string | 必填 | OpenAI-compatible Chat Completions 地址 |
| model | string | 必填 | 远程模型名 |
| apiKey | string | 无 | 运行时凭证;无自定义 Authorization 时自动生成 Bearer 头,不要序列化或硬编码 |
| headers | Record<string, string> | {} | 自定义请求头;可自行提供 Authorization |
| prompt | string | 内置车牌定位提示词 | 自定义模型提示词 |
| coordinateFormat | 'auto' \| 'normalized' \| 'image' \| 'thousand' | 'auto' | 模型返回点坐标单位 |
| maxImageSide | number | 1600 | 上传图片最长边,最小按 320 处理 |
| imageMimeType | 'image/jpeg' \| 'image/png' | 'image/jpeg' | 上传编码格式 |
| imageQuality | number | 0.9 | JPEG/PNG Canvas 编码质量;最终限制在 0.5..1 |
| timeout | number | 45000 | 请求超时时间,最小 1000ms |
| maxTokens | number | 服务默认 | 写入请求的 max_tokens |
| temperature | number | 服务默认 | 写入请求的 temperature |
| extraBody | Record<string, unknown> | {} | 合并进请求体的额外字段 |
远程服务返回的 JSON 可以是四角点、数组、plates/detections 数组、bbox 或常见 Chat Completions 文本 JSON。跨域请求还必须满足 CORS。不要把长期 API Key 直接放进前端生产代码。
自定义检测器
import type { PlateDetector } from '@composy/plate-editor-core'
const detector: PlateDetector = {
backend: 'custom',
async detect({ width, height }, options) {
const x = width * 0.3
const y = height * 0.55
const plateWidth = width * 0.4
const plateHeight = height * 0.09
return [{
corners: [
{ x, y },
{ x: x + plateWidth, y },
{ x: x + plateWidth, y: y + plateHeight },
{ x, y: y + plateHeight },
],
confidence: options?.maxResults ? 0.96 : 0.9,
type: 'single-line',
source: 'detected',
}]
},
}
await editor.setDetector(detector)
await editor.detectPlates({ precise: true, replace: true })PlateDetectorContext:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| image | CanvasImageSource | 当前原图 |
| width | number | 原图宽度 |
| height | number | 原图高度 |
detect() 返回 PlateDetection[]。角点需要表示同一个四边形,Core 会重新排序、校验、过滤越界和不合理比例。maxResults 是建议上限,最终仍会受到编辑器 maxPlates 限制。
车牌图层与样式
Plate 数据结构
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | string | 图层唯一 ID |
| corners | PlateCorners | [左上, 右上, 右下, 左下],会被 Core 归一化 |
| type | 'single-line' \| 'double-line' \| 'motorcycle' \| 'unknown' | 车牌类型 |
| source | 'detected' \| 'manual' \| 'restored' | 图层来源 |
| confidence | number | 检测置信度;手动图层默认 1 |
| cornerConfidences | [number, number, number, number] | 四角置信度 |
| visible | boolean | 是否渲染 |
| locked | boolean | 锁定后不能拖拽角点、更新角点或删除 |
| style | PlateStyle | 内容、颜色、边框、阴影和安装件 |
手动添加和更新。setAddMode(true) 进入原有的拖拽矩形框选;setAddMode('corners') 后依次点击四个角点,第四次点击完成四边形。两种方式创建的图层都可以继续拖动四边和四角手柄调整;按 Esc 可以取消当前添加。
editor.setAddMode(true)
// 另一种方式:依次点击左上、右上、右下、左下四个角点
editor.setAddMode('corners')
const plate = editor.addPlate({
corners: [
{ x: 420, y: 520 },
{ x: 840, y: 515 },
{ x: 850, y: 625 },
{ x: 410, y: 630 },
],
style: {
text: '粤A·72518',
backgroundColor: '#1d63d8',
textColor: '#ffffff',
},
})
editor.updatePlate(plate.id, {
visible: true,
locked: false,
corners: [
{ x: 425, y: 520 },
{ x: 838, y: 516 },
{ x: 848, y: 622 },
{ x: 414, y: 631 },
],
})
editor.updatePlateStyle(plate.id, { text: '粤A·88888' })addPlate() 的 record 参数默认为 true;内部检测流程会传 false,业务一般不需要修改它。超出 maxPlates、四边形无效或锁定图层更新角点时会返回 null、false 或抛出对应错误,具体见方法表。
PlateStyle 全字段
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| contentMode | 'text' \| 'image' \| 'both' | 'text' | 文字、图片或两者 |
| image | PlateImageStyle | 无 | 图片内容;source 是 data URL 或 URL |
| backgroundColor | string | '#1d63d8' | 背景色 |
| backgroundColorEnd | string | 无 | 设置后与起始色形成渐变 |
| gradientAngle | number | 0 | 渐变角度 |
| text | string | '京A·12345' | 车牌文字 |
| textColor | string | '#ffffff' | 文字颜色 |
| fontFamily | string | DIN/Arial/微软雅黑字体栈 | Canvas 字体栈 |
| fontSize | number | 58 | 逻辑字体大小 |
| fontWeight | number \| string | 700 | Canvas 字重 |
| letterSpacing | number | 2 | 逻辑字间距 |
| textAlign | 'left' \| 'center' \| 'right' | 'center' | 文字对齐 |
| aspectRatio | number | 22 / 7 | 车牌宽高比 |
| contentInset | number \| { horizontal?: number; vertical?: number } | 0 | 文字/图片内容安全边距,不移动四角 |
| opacity | number | 0.98 | 背景透明度,绘制时限制在 0..1 |
| border | PlateBorder | 开启、浅色、宽度 2 | 外框及可选内框 |
| mountingBolts | PlateMountingBolts | 关闭(默认不显示) | 四颗安装螺帽为可选细节,需显式 enabled: true 才会绘制 |
| radius | number | 5 | 圆角 |
| feather | number | 1 | 边缘柔化 |
| inset | number | 0 | 车牌表面绘制内缩 |
| shadow | { enabled; color; blur; offsetX; offsetY } | 开启,黑色、blur 8、Y 3 | 表面阴影 |
嵌套类型:
interface PlateImageStyle {
source: string
fit?: 'fill' | 'contain' | 'cover'
opacity?: number
}
interface PlateBorder {
enabled: boolean
color: string
width: number
innerColor?: string
innerWidth?: number
}
interface PlateMountingBolts {
enabled?: boolean
size?: number
inset?: number
color?: string
}文字、图片和混合内容
editor.updatePlateStyle(plate.id, {
contentMode: 'both',
text: '粤A·72518',
backgroundColor: '#1d63d8',
backgroundColorEnd: '#0b3f9a',
gradientAngle: 90,
textColor: '#ffffff',
fontFamily: 'Arial, sans-serif',
fontSize: 56,
fontWeight: 700,
letterSpacing: 2,
contentInset: { horizontal: 18, vertical: 7 },
mountingBolts: { enabled: true, size: 4.5, inset: 16 },
})
await editor.setPlateImage(plate.id, logoFile, {
fit: 'cover',
opacity: 0.96,
contentMode: 'both',
})
editor.clearPlateImage(plate.id)setPlateImage() 会把输入图片转成 data URL,便于 serialize() 和 restore()。fit 支持 fill、contain、cover;opacity 会限制在 0..1;contentMode 只接受 'image' 或 'both'。清除图片后会恢复为文字模式。
预设
包内预设导出为 BUILT_IN_PLATE_PRESETS,默认不会自动加入编辑器。内置预设包括标准蓝牌、新能源绿牌、商用黄牌、白色警车牌和黑色使馆牌。
const customPresets = [
{
id: 'fleet-blue',
name: '车队蓝牌',
description: '企业车队替换样式',
style: {
contentMode: 'text',
backgroundColor: '#173d7a',
text: '粤A·72518',
textColor: '#ffffff',
aspectRatio: 4,
},
},
]
const editor = new Editor({
container: '#plate-editor',
presets: customPresets,
defaultPresetId: 'fleet-blue',
showBuiltInPresets: false,
})
const preset = editor.registerPreset({
id: 'fleet-yellow',
name: '车队黄牌',
style: { backgroundColor: '#e0a62b', textColor: '#17202a' },
})
editor.setDefaultPreset(preset.id)
await editor.applyPreset(preset.id, editor.selectedPlate?.id)
editor.updatePreset(preset.id, { description: '更新后的说明' })
editor.removePreset(preset.id)| API | 行为 |
| --- | --- |
| presets | 初始化时注册;自定义 ID 与内置 ID 相同会覆盖内置项 |
| showBuiltInPresets | true 才加入内置预设,默认 false |
| defaultPresetId | 指定新图层使用的默认预设;无效 ID 会被忽略 |
| registerPreset(input) | 注册自定义预设;重复 ID 会抛错 |
| updatePreset(id, input) | 更新预设;找不到返回 null |
| removePreset(id) | 删除非内置预设;内置或不存在返回 false |
| setDefaultPreset(id) | 设为默认;传 null 清空;无效 ID 返回 false |
| applyPreset(id, plateId?) | 应用到指定图层;未指定时使用当前选中图层;找不到返回 null |
视口、历史和状态
editor.setZoom(1.5)
editor.zoomBy(1.25)
editor.resetViewport()
editor.setComparison(true)
editor.setAddMode(true)
if (editor.canUndo) editor.undo()
if (editor.canRedo) editor.redo()
const state = editor.serialize()
localStorage.setItem('plate-editor-state', JSON.stringify(state))
await editor.restore(JSON.parse(localStorage.getItem('plate-editor-state')!))getViewport() 返回:
| 字段 | 说明 |
| --- | --- |
| fitScale | 当前容器适配原图的比例,不超过 1 |
| scale | fitScale * zoom,Canvas 当前视觉比例 |
| zoom | 用户缩放因子,范围会限制在 0.25..8 |
| x / y | 视口平移量 |
PlateEditorState:
interface PlateEditorState {
version: 1
imageSource?: string
width: number
height: number
plates: Plate[]
selectedPlateId: string | null
}只有字符串图片源会写入 imageSource;File、Blob、ArrayBuffer 和 ImageBitmap 不能仅靠状态恢复,需要业务同时保存原始图片或先转成 URL/data URL。restore() 遇到不支持的 version 会 reject。
导出、下载和保存
PlateExportOptions
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| type | 'image/png' \| 'image/jpeg' \| 'image/webp' | 原图格式 | 输出 MIME 类型;未指定时从最近一次导入的 JPG、PNG 或 WebP 推断 |
| quality | number | 0.8(实际有编辑或显式指定导出参数时) | JPEG/WebP 质量;最终限制在 0.1..1 |
| useOriginalResolution | boolean | 未设置时按原图比例 | 是否以原图作为导出基准;false 时仍由 scale 控制 |
| scale | number | 1 | 相对原图的输出比例,限制在 0.05..8 |
| maxWidth | number | 无 | 限制输出最大宽度,并按比例缩小 |
| maxHeight | number | 无 | 限制输出最大高度,并按比例缩小 |
| backgroundColor | string | 透明 | 导出底色;适合 JPEG |
// 不传 type:JPG 导入后仍导出 JPG,PNG/WebP 同理。
const blob = await editor.export({ quality: 0.8, useOriginalResolution: true })
// 也可以显式指定格式与质量。
const jpeg = await editor.export({
type: 'image/jpeg',
quality: 0.72,
useOriginalResolution: true,
maxWidth: 4096,
backgroundColor: '#ffffff',
})
await editor.download('vehicle-plate', { type: 'image/png' })
await editor.save('vehicle-plate', { type: 'image/png' })export() 只返回 Blob,不触发下载。download() 导出后通过浏览器 <a download> 下载。save() 是语义化保存入口:传入 handler 时交给 handler,否则调用默认下载。
当图片由 File 或 Blob 导入、且没有添加车牌或指定格式、质量、尺寸、底色等导出参数时,export() / download() / save() 会直接复用原始 Blob,不进行无意义的二次编码,因此“导入后直接保存”不会因为编码开销而变大。只要发生编辑或显式指定导出参数,才会重新编码;JPEG/WebP 的默认质量为 0.8。
自定义保存回调:
await editor.save('vehicle-plate', { type: 'image/png' }, async context => {
const body = new FormData()
body.append('file', context.blob, `${context.filename}.png`)
await fetch('/api/plate-images', { method: 'POST', body })
// 如果还需要本地下载,再调用:
// await context.download()
})PlateEditorSaveContext:
| 字段 | 说明 |
| --- | --- |
| blob | 已完成车牌透视合成的结果 |
| filename | 不含扩展名的业务文件名 |
| options | 经过默认值补全后的保存选项 |
| download() | 对同一个 Blob 执行默认浏览器下载 |
全部事件
使用 Editor.on()/off() 或 PlateEditorStudio.on()/off() 订阅:
| 事件 | 载荷 | 触发时机 |
| --- | --- | --- |
| lifecycle:ready | { width, height } | 初始布局和历史状态准备完成;构造函数之后异步触发 |
| lifecycle:before-destroy | undefined | destroy() 开始释放资源前 |
| lifecycle:destroy | undefined | DOM、监听器和检测器资源释放后 |
| destroy | undefined | Core 销毁事件;与 lifecycle:destroy 同一销毁流程内触发 |
| image:load-start | { source } | 开始加载图片 |
| image:loaded | { width, height } | 图片加载并完成布局 |
| image:error | { error } | 图片加载、格式、尺寸、跨域等错误 |
| image:clear | undefined | clearImage() 清空图片和图层 |
| model:ready | { backend } | 检测器预加载完成;backend 为 none/heuristic/webgpu/wasm/remote/custom |
| model:error | { error } | 检测器预加载失败 |
| detect:start | { precise } | 开始调用检测器 |
| detect:complete | { plates, precise } | 检测完成并生成图层 |
| detect:error | { error } | 检测器或候选解析失败 |
| interaction:start | { type, plateId? } | 开始角点、边、移动、平移或手动框选 |
| interaction:end | { type, plateId?, changed } | 交互结束;changed 表示是否产生编辑变化 |
| viewport:change | { fitScale, scale, zoom, x, y } | 缩放、平移、重置或容器尺寸变化 |
| plate:add | { plate } | 新增图层 |
| plate:select | { plate } | 当前选中图层变化;可为 null |
| plate:update | { plate } | 图层角点、样式、可见性或锁定状态变化 |
| plate:remove | { plateId } | 删除图层 |
| preset:change | { presets, defaultPresetId } | 预设注册、更新、删除或默认项变化 |
| history:change | { canUndo, canRedo } | 历史指针或快照变化 |
| render:start | { width, height } | 预览 Canvas 开始渲染 |
| render:complete | { width, height } | 预览 Canvas 完成渲染 |
| export:start | { options } | 开始导出;无编辑的 File/Blob 可能直接复用原始 Blob |
| export:complete | { blob, options } | 导出 Blob 完成 |
| export:error | { error } | Canvas 编码或跨域导出失败 |
| save:start | { filename, options } | save() 开始 |
| save:complete | PlateEditorSaveContext | handler 完成或默认下载完成 |
| save:error | { error, filename, options } | handler、导出或下载流程失败 |
| change | { plates, selectedPlateId } | 车牌图层或选中项发生可持久化变化 |
| dialog:open | { type: 'editor' \| 'presets' } | 仅 Studio:编辑弹窗或预设弹窗打开 |
| dialog:close | { type: 'editor' \| 'presets' } | 仅 Studio:编辑弹窗或预设弹窗关闭 |
例子:
function onChange(payload: PlateEditorEvents['change']) {
localStorage.setItem('plates', JSON.stringify(payload.plates))
}
editor.on('change', onChange)
editor.on('image:error', ({ error }) => console.error(error))
editor.on('export:complete', ({ blob }) => console.log('exported', blob.size))
editor.off('change', onChange)全部实例 API
Editor 属性
| API | 类型/返回值 | 说明 |
| --- | --- | --- |
| canvas | HTMLCanvasElement | 底层结果 Canvas |
| overlay | HTMLCanvasElement | 交互手柄和覆盖层 Canvas |
| width / height | number | 当前原图尺寸 |
| platesList | Plate[] | 当前图层的深拷贝列表 |
| selectedPlate | Plate \| null | 当前图层的深拷贝 |
| canUndo / canRedo | boolean | 是否可以撤销/重做 |
| isEmpty | boolean | 当前是否没有图片 |
| addMode | false \| 'drag' \| 'corners' | 当前新增车牌模式 |
| detectorBackend | PlateDetector['backend'] | 当前检测器后端 |
| presetsList | PlatePreset[] | 全部预设的深拷贝列表 |
| defaultPreset | PlatePreset \| null | 当前默认预设的深拷贝 |
Editor 生命周期、事件与视口
| API | 签名 | 返回值/说明 |
| --- | --- | --- |
| on | on(event, handler) | 注册事件;同一个 handler 不重复添加 |
| off | off(event, handler) | 移除事件 |
| getViewport | getViewport() | PlateEditorViewport |
| setZoom | setZoom(zoom) | void;限制在 0.25..8 |
| zoomBy | zoomBy(factor) | void;factor 必须为正有限数 |
| resetViewport | resetViewport() | void;恢复 zoom、x、y |
| render | render() | void;强制重新绘制当前预览 |
| destroy | destroy() | Promise<void>;幂等释放资源 |
Editor 图片、检测和模型
| API | 签名 | 返回值/说明 |
| --- | --- | --- |
| loadImage | loadImage(source) | Promise<void>;加载成功后清空旧图层并重置视口/历史 |
| clearImage | clearImage() | void;清空图片、图片缓存、图层和历史 |
| preload | preload() | Promise<void>;预加载当前检测器 |
| setDetector | setDetector(detector) | Promise<void>;成功后替换旧检测器 |
| setModel | setModel(model & { url: string }) | Promise<void>;创建并切换 ONNX 检测器 |
| detectPlates | detectPlates({ precise?, replace? }) | Promise<Plate[]>;默认 precise: false、replace: true |
| detectWithLargeModel | detectWithLargeModel(options, { replace? }) | Promise<Plate[]>;临时使用远程检测器,不替换当前检测器 |
| setAddMode | setAddMode(active?: boolean \| 'drag' \| 'corners') | void;true/'drag' 开启拖拽矩形框选,'corners' 开启四角点选,false 取消 |
| setComparison | setComparison(showOriginal) | void;显示原图与结果对比 |
Editor 图层与预设
| API | 签名 | 返回值/说明 |
| --- | --- | --- |
| registerPreset | registerPreset(input) | PlatePreset;重复 ID 抛错 |
| updatePreset | updatePreset(id, input) | PlatePreset \| null |
| removePreset | removePreset(id) | boolean;内置预设不能删除 |
| setDefaultPreset | setDefaultPreset(id \| null) | boolean;null 清空默认项 |
| applyPreset | applyPreset(presetId, plateId?) | Promise<Plate \| null>;默认当前选中图层 |
| addPlate | addPlate(input, record?) | Plate;无效四边形或超过数量会抛错 |
| updatePlate | updatePlate(id, input) | Plate \| null;锁定图层不能更新角点 |
| updatePlateStyle | updatePlateStyle(id, style) | Plate \| null;样式快捷更新 |
| removePlate | removePlate(id) | boolean;锁定或不存在返回 false |
| selectPlate | selectPlate(id \| null) | void;无效 ID 会清除选中项 |
| copyStyleToAll | copyStyleToAll(sourceId?) | void;复制给其它未锁定图层 |
| setPlateImage | setPlateImage(id, source, options?) | Promise<Plate \| null> |
| clearPlateImage | clearPlateImage(id) | Plate \| null;清除图片并恢复文字模式 |
Editor 历史、状态与输出
| API | 签名 | 返回值/说明 |
| --- | --- | --- |
| undo | undo() | boolean;没有历史时返回 false |
| redo | redo() | boolean;没有可重做历史时返回 false |
| serialize | serialize() | PlateEditorState;返回可 JSON 化状态 |
| restore | restore(state) | Promise<void>;支持版本 1 |
| renderPreview | renderPreview() | Promise<HTMLCanvasElement>;创建独立结果 Canvas |
| export | export(options?) | Promise<Blob>;不下载 |
| download | download(filename?, options?) | Promise<void>;浏览器下载 |
| save | save(filename?, options?, handler?) | Promise<void>;handler 优先,否则下载 |
PlateEditorStudio API
| API | 返回值/说明 |
| --- | --- |
| editor | 底层 Editor 实例 |
| element | Studio 根 DOM 元素 |
| on / off | 代理全部 Core 事件,并支持 dialog:open、dialog:close |
| openPicker() | 打开文件选择器 |
| loadImage(source) | 加载图片,dialog 模式会打开编辑弹窗;自动检测由 autoDetect 控制,且仅在配置了 ONNX 模型或自定义 detector 时运行 |
| detectPlates(options?) | 在非只读、有图片时运行检测;不可运行时返回空数组。未配置 ONNX 模型或自定义 detector 时,工具栏「重新识别」按钮会自动隐藏 |
| applyPreset(presetId, plateId?) | 设置默认预设并应用到目标图层;多图层时会同步样式 |
| preload() | 预加载当前检测器 |
| setModel(model) | 替换 ONNX 模型 |
| setTheme(theme) / getTheme() | 设置/获取 Studio 主题 |
| setPrimaryColor(color?) / getPrimaryColor() | 设置/获取主色;不传色恢复默认主题色 |
| autoDetectionEnabled / setAutoDetect(enabled) | 获取/设置自动检测开关 |
| setAddMode(active?) | 切换拖拽矩形框选、四角点选或取消添加 |
| zoomBy(factor) / resetViewport() | 委托底层视口 API |
| export / download / save | 委托底层输出 API |
| destroy() | 释放底层 Editor、事件监听、portal 和 DOM |
根入口导出
运行时导出
import {
Editor,
PlateEditorStudio,
BUILT_IN_PLATE_PRESETS,
createManualDetector,
createHeuristicDetector,
createOnnxPlateDetector,
createLargeModelPlateDetector,
loadOnnxRuntime,
lucideIcon,
ensureEditorStyles,
createPlateSurface,
warpPlate,
clamp,
distance,
getBounds,
insetCorners,
isValidQuad,
normalizeCorners,
pointInPolygon,
polygonArea,
quadSize,
translateCorners,
} from '@composy/plate-editor-core'| 导出 | 作用 |
| --- | --- |
| Editor | 框架无关编辑器 |
| PlateEditorStudio | 原生 DOM 完整工作台 |
| BUILT_IN_PLATE_PRESETS | 包内预设数据 |
| createManualDetector | 空检测器 |
| createHeuristicDetector | 启发式检测器 |
| createOnnxPlateDetector | ONNX 检测器 |
| createLargeModelPlateDetector | 远程视觉检测器 |
| loadOnnxRuntime | 加载 ONNX Runtime |
| createPlateSurface / warpPlate | 创建车牌表面、透视合成工具 |
| lucideIcon | 创建内置或自定义 SVG 图标 |
| ensureEditorStyles | 手动注入编辑器 Canvas 样式;Editor 会自动调用 |
| 几何函数 | 坐标裁剪、距离、边界、内缩、合法性、归一化、点面判断、面积、尺寸、平移 |
类型入口包括 Plate、PlateStyle、PlatePreset、PlateDetector、ModelOptions、LargeModelOptions、PlateEditorOptions、PlateEditorStudioOptions、PlateExportOptions、PlateEditorEvents、PlateEditorStudioEvents 以及所有嵌套类型。
UMD、示例与构建
Core UMD
<div id="plate-editor" style="height: 640px"></div>
<script src="./dist/index.min.js"></script>
<script>
const studio = new LDesignPlateEditor.PlateEditorStudio({
container: '#plate-editor',
autoDetect: false,
mode: 'inline',
})
</script>全局对象名为 LDesignPlateEditor。UMD 不会自动提供模型权重、ONNX Runtime 或 WASM 文件。完整页面参考 packages/core/examples/umd 和 dialog.html。
工作区命令
pnpm --dir libraries/plate-editor/packages/core run type-check
pnpm --dir libraries/plate-editor/packages/core run build
pnpm --dir libraries/plate-editor/packages/core run lint:check相关示例:
examples/README.md:示例索引;examples/esm-direct/main.ts:直接 ESM;examples/cjs/main.cjs:CommonJS;examples/umd/index.html:UMD 基础页面;examples/umd/dialog.html:dialog 模式;examples/umd/custom-style.html:自定义样式。
完整工作区 Playground 位于 ../../playground,包含 Core/Vanilla 演示、Vue 演示、配置示例、文档和 API 页面。
销毁与资源管理
组件或页面卸载时必须调用:
await editor.destroy()
await studio.destroy()销毁会移除 Canvas 事件、ResizeObserver、检测器资源、ImageBitmap、图片缓存、Studio 事件、DOM 包装节点和 body 下的弹窗 portal。destroy() 是幂等的,可以安全重复调用。
