npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@composy/plate-editor-core

v0.0.4

Published

Framework-neutral license plate replacement engine

Downloads

38

Readme

@composy/plate-editor-core

@composy/plate-editor-core 是一个框架无关的浏览器车牌图片编辑引擎。它提供两层能力:

  • Editor:只负责 Canvas 编辑、图片加载、检测器、车牌图层、透视渲染、预设、历史记录、状态序列化和导出;适合原生 DOM、React、Vue、Web Components 或自行编写 UI。
  • PlateEditorStudio:在 Editor 之上提供完整的原生 DOM 工作台,包括工具栏、导入、识别、矩形框选、四角点选、预设弹窗、缩放、撤销/重做、原图对比、主题和保存。

所有图片编辑在浏览器 Canvas 中完成。ONNX 推理默认也在浏览器内执行;远程大模型检测是显式调用的可选能力,不会自动上传图片。

车牌检测模型、模型权重、模型输出格式和模型训练数据不由本包提供。生产环境应使用适合目标国家、车牌类型、拍摄角度和光照条件的模型,并让用户确认自动检测结果。

目录

安装与运行环境

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
  | ImageBitmap
await 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,并检查文件大小。所有来源都会检查图片尺寸和总像素数。超限或加载失败时:

  1. 触发 image:error;
  2. loadImage() Promise reject;
  3. 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
}

优先级:

  1. options.detector;
  2. options.model.url 创建的 ONNX 检测器;
  3. options.useHeuristicFallback === true 创建的启发式检测器;
  4. 其它情况使用不返回候选的手动检测器。

检测结果进入编辑器前还会经过置信度、四边形有效性、车牌长宽比和最大数量过滤。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

相关示例:

完整工作区 Playground 位于 ../../playground,包含 Core/Vanilla 演示、Vue 演示、配置示例、文档和 API 页面。

销毁与资源管理

组件或页面卸载时必须调用:

await editor.destroy()
await studio.destroy()

销毁会移除 Canvas 事件、ResizeObserver、检测器资源、ImageBitmap、图片缓存、Studio 事件、DOM 包装节点和 body 下的弹窗 portal。destroy() 是幂等的,可以安全重复调用。