@gulibs/beautify-qrcode-hooks
v0.0.5
Published
React Hooks,基于 [qr-code-styling](https://www.npmjs.com/package/qr-code-styling),支持单码/批量生成、Canvas 与 SVG 双模式预览、内置背景图与文字叠加插件、PNG/JPEG/WebP/SVG 导出,以及可扩展的插件体系。
Downloads
63
Readme
@gulibs/beautify-qrcode-hooks
React Hooks,基于 qr-code-styling,支持单码/批量生成、Canvas 与 SVG 双模式预览、内置背景图与文字叠加插件、PNG/JPEG/WebP/SVG 导出,以及可扩展的插件体系。
安装
pnpm add @gulibs/beautify-qrcode-hooks qr-code-styling
# 对等依赖:react >=18, react-dom >=18快速开始
import { useRef, useEffect } from 'react'
import { useBeautifyQRCode } from '@gulibs/beautify-qrcode-hooks'
function QRDemo() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const { drawPreview, download } = useBeautifyQRCode({
content: 'https://example.com',
dimensions: { width: 300, height: 300 },
})
useEffect(() => {
if (canvasRef.current) drawPreview(canvasRef.current)
}, [drawPreview])
return (
<>
<canvas ref={canvasRef} style={{ width: 300, height: 300 }} />
<button onClick={() => download({ name: 'qrcode', extension: 'png' })}>
下载 PNG
</button>
</>
)
}
<canvas>的 CSS 宽高(style)控制显示尺寸,canvas.width/height属性由 Hook 内部自动设置为 CSS 尺寸 × DPR,无需手动赋值。
核心概念
受控 vs 非受控设置
Hook 支持两种状态管理模式:
受控模式(推荐):将外部 settings 对象直接传入,Hook 始终使用该值,无需调用 updateSettings。外部状态变化时直接触发重新渲染。
const settings = useMemo<BeautificationSettings>(() => ({
qrCodeStyle: { foregroundColor: '#333', margin: 8 },
}), [someState])
const { drawPreview } = useBeautifyQRCode({ content, dimensions, settings })非受控模式:不传 settings,传入 initialSettings 作为初始值,之后通过 updateSettings 修改内部状态。适合 Hook 内部完全自管理状态的场景。
const { drawPreview, updateSettings } = useBeautifyQRCode({
content,
dimensions,
initialSettings: { qrCodeStyle: { margin: 8 } },
})
// 更新单个字段
updateSettings(prev => ({
...prev,
qrCodeStyle: { ...prev.qrCodeStyle, margin: 16 },
}))同时传入
settings和initialSettings/updateSettings时,settings优先,updateSettings仍可写入内部状态但不影响外部受控值。
DPR 与像素一致性
预览(Canvas 模式):
drawPreview自动读取window.devicePixelRatio,将 canvas buffer 设置为 CSS 尺寸 × DPR(例如 300 px @ DPR=2 → buffer 600×600)。ctx.scale(dpr, dpr)使所有绘制坐标保持 CSS 像素,Retina 屏幕下自动锐利。- Canvas 的 CSS 显示尺寸(
style.width/height)由业务侧的 JSX 控制,Hook 不修改。
导出(PNG/JPEG/WebP):
getRawData、download、exportAsDataURL均接受scale参数(默认 2),以dimensions × scale的物理像素分辨率导出,确保下载图与预览清晰度一致。
SVG 模式:矢量格式,任意缩放不失真,DPR 与 scale 对 SVG 导出均无意义。
Canvas 与 SVG 坐标系一致性
两种预览模式共享相同的坐标系:
- 外层容器尺寸为
dimensions.width × dimensions.height。 - QR 区域(
qrX,qrY,qrSize)由computePreviewRect计算,扣除文字带后居中排列。 - Canvas 中,QR 图像绘制到
ctx.drawImage(img, qrX, qrY, qrSize, qrSize);SVG 中,QR SVG 元素包裹在<g transform="translate(qrX, qrY)">中。 - 插件的
svgExtension收到的options.width/height为外层容器尺寸(不是qrSize),确保边框、装饰类扩展坐标与 Canvas 插件对齐。
无闪烁渲染
drawPreview 采用双缓冲策略:
- 所有异步渲染(生成 QR、加载图片、插件 beforeRender/afterRender)在离屏 canvas 上完成。
- 完成后同步执行
visibleCanvas.width = offscreen.width+ctx.drawImage(offscreen, 0, 0),两行代码在同一 JS 任务内,浏览器无法在两者之间插入绘制帧,因此不出现空白闪烁。
竞态保护:每次调用 drawPreview/renderPreviewToSvg 都分配单调递增的渲染 ID;若更新的调用在旧调用完成前到达,旧调用在每个异步检查点放弃(不绘制),确保屏幕永远显示最新状态。
useBeautifyQRCode
单码 QR 生成与预览的主 Hook。
入参 UseBeautifyQRCodeOptions
| 属性 | 类型 | 默认 | 说明 |
|------|------|------|------|
| content | string | — | 受控 QR 内容。传入后 setContent 为 no-op。 |
| contentForQR | string | '' | 未传 content 时的默认内容(仅用于初始化内部 state)。 |
| settings | BeautificationSettings | — | 受控样式配置。传入后 Hook 直接使用,忽略内部 state 与 initialSettings。 |
| initialSettings | BeautificationSettings | {} | 非受控模式的初始样式(仅初始化内部 state 一次)。 |
| dimensions | { width: number; height: number } | { width: 300, height: 300 } | 预览与导出的容器尺寸(CSS 像素)。 |
| previewMode | 'canvas' \| 'svg' | 'canvas' | 用于消费侧初始化 mode 状态的提示值,不影响 Hook 内部行为。 |
| extension | ExtensionFunction | — | 单个 SVG 扩展函数,在所有插件 svgExtension 之后执行。 |
| plugins | BeautifyQRCodePlugin[] | — | 静态插件数组,Hook 内部自动构建 registry。 |
| pluginRegistry | PluginRegistry \| null | — | 外部 registry(优先于 plugins);适合运行时 register/unregister。 |
| onError | QRCodeErrorCallback | — | 预览或导出出错时回调,接收 (error, { source }) 。 |
plugins与pluginRegistry同时传入时,pluginRegistry生效,plugins被忽略。
返回值 UseBeautifyQRCodeReturn
状态
| 属性 | 类型 | 说明 |
|------|------|------|
| settings | BeautificationSettings | 当前生效的完整配置(受控时为传入值,非受控时为内部 state)。 |
| updateSettings | (value: BeautificationSettings \| ((prev) => BeautificationSettings)) => void | 更新内部 state(受控模式下内部 state 不影响渲染,但仍可写入;非受控模式的主要更新接口)。 |
| content | string | 当前内容(受控时为 content prop,否则为内部 state)。 |
| setContent | (value: string \| ((prev) => string)) => void | 非受控场景下更新内容。受控时(传入 content prop)为 no-op。 |
| effectiveContent | string | 实际传给 QR 生成器的内容,优先级:content prop > 内部 state > contentForQR。 |
| dimensions | { width: number; height: number } | 当前容器尺寸(来自 dimensionsProp 或默认 300×300)。 |
| previewMode | 'canvas' \| 'svg' | 与入参 previewMode 一致,固定值。 |
| canUseSvgPreview | boolean | 恒为 true,保留用于兼容性。 |
预览
| 方法 | 签名 | 说明 |
|------|------|------|
| drawPreview | (canvas: HTMLCanvasElement) => Promise<void> | 将 QR 绘制到指定 canvas。自动 DPR 缩放,双缓冲无闪烁,竞态安全。内容为空时立即返回。 |
| renderPreviewToSvg | (container: HTMLElement) => Promise<void> | 在指定容器内挂载 SVG 预览(清空后追加)。竞态安全。内容为空时立即返回。 |
导出
| 方法 | 签名 | 说明 |
|------|------|------|
| getRawData | (extension?: FileExtension, scale?: number) => Promise<Blob \| null> | 获取 Blob。extension 默认 'png';scale 默认 2(SVG 忽略 scale)。内容为空时返回 null。 |
| download | (opts?: { name?: string; extension?: FileExtension; scale?: number }) => Promise<void> | 触发浏览器下载。name 默认 'qr',extension 默认 'png',scale 默认 2(SVG 忽略)。 |
| exportAsDataURL | (extension?: FileExtension, scale?: number) => Promise<string> | 导出为 Data URL 字符串。extension 默认 'png',scale 默认 2(SVG 忽略)。失败或内容空时返回 ''。 |
scale 说明:
scale=2时,300×300 容器导出为 600×600 px 图像。建议scale取 2(屏幕)或 3/4(印刷)。SVG 无损矢量,无需 scale。
完整示例
import { useRef, useEffect, useState, useMemo } from 'react'
import { useBeautifyQRCode } from '@gulibs/beautify-qrcode-hooks'
import type { BeautificationSettings, FileExtension } from '@gulibs/beautify-qrcode-hooks'
function QREditor() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const svgRef = useRef<HTMLDivElement>(null)
const [mode, setMode] = useState<'canvas' | 'svg'>('canvas')
const [content, setContent] = useState('https://example.com')
const [margin, setMargin] = useState(10)
// 受控模式:settings 随 margin 变化自动同步
const settings = useMemo<BeautificationSettings>(() => ({
qrCodeStyle: {
foregroundColor: '#1e40af',
errorCorrectionLevel: 'Q',
margin,
dotType: 'extra-rounded',
},
}), [margin])
const { drawPreview, renderPreviewToSvg, download, exportAsDataURL } =
useBeautifyQRCode({ content, dimensions: { width: 300, height: 300 }, settings })
useEffect(() => {
if (mode === 'canvas' && canvasRef.current) drawPreview(canvasRef.current)
}, [mode, drawPreview])
useEffect(() => {
if (mode === 'svg' && svgRef.current) renderPreviewToSvg(svgRef.current)
}, [mode, renderPreviewToSvg])
return (
<div>
{mode === 'canvas'
? <canvas ref={canvasRef} style={{ width: 300, height: 300 }} />
: <div ref={svgRef} style={{ width: 300, height: 300 }} />}
<input value={margin} type="range" min={0} max={40}
onChange={e => setMargin(Number(e.target.value))} />
<button onClick={() => download({ name: 'qr', extension: 'png', scale: 3 })}>
下载 3× PNG
</button>
<button onClick={() => download({ extension: 'svg' })}>
下载 SVG
</button>
</div>
)
}useBatchBeautify
批量生成与导出,每条数据独立配置。
入参 UseBatchBeautifyOptions
| 属性 | 类型 | 默认 | 说明 |
|------|------|------|------|
| items | BatchBeautifyItem[] | 必填 | 数据项数组,见下方 BatchBeautifyItem 说明。 |
| dimensions | { width: number; height: number } | { width: 300, height: 300 } | 每条的预览/导出容器尺寸。 |
| format | FileExtension | 'png' | getDataURL / exportAll 的默认格式。 |
| extension | ExtensionFunction | — | 所有条共用的 SVG 扩展。 |
| plugins | BeautifyQRCodePlugin[] | — | 所有条共用的插件数组。 |
| pluginRegistry | PluginRegistry \| null | — | 外部 registry,优先于 plugins。 |
| logoMap | Map<number, string> | — | 按条索引指定 logo URL,覆盖 item.initialSettings.logo.url。 |
| onError | QRCodeErrorCallback | — | 出错时回调。 |
BatchBeautifyItem
| 属性 | 类型 | 说明 |
|------|------|------|
| id | string \| number | 唯一标识。 |
| content | string | 该条的 QR 内容。 |
| title | string? | 可选显示标题。 |
| initialSettings | BeautificationSettings? | 该条的样式配置;可被 logoMap 的 logo 覆盖。 |
返回值 UseBatchBeautifyReturn
进度状态
| 属性 | 类型 | 说明 |
|------|------|------|
| isExporting | boolean | exportAll 执行期间为 true。 |
| progress | number | 0–1,exportAll 的完成比例。 |
| completedCount | number | exportAll 已完成的条数。 |
方法
| 方法 | 签名 | 说明 |
|------|------|------|
| getItemPreview | (index: number) => ItemPreviewHandle | 获取指定条(按数组下标)的预览句柄。 |
| exportAll | (scale?: number) => Promise<Blob[]> | 批量导出所有条为 Blob 数组;scale 默认 2;格式由 format 决定。返回与 items 等长的数组,失败条为空 Blob。 |
ItemPreviewHandle
| 方法 | 签名 | 说明 |
|------|------|------|
| drawPreview | (canvas: HTMLCanvasElement) => Promise<void> | 将该条绘制到指定 canvas;双缓冲无闪烁,自动 DPR。 |
| getDataURL | (ext?: FileExtension) => Promise<string> | 该条以指定格式(默认 format)导出为 Data URL。 |
| renderToSvg | (container: HTMLElement) => Promise<void> | 将该条渲染为 SVG 并挂载到容器;QR 元素通过 position: absolute 定位到正确区域。 |
完整示例
import { useRef, useEffect } from 'react'
import { useBatchBeautify } from '@gulibs/beautify-qrcode-hooks'
const items = [
{ id: 1, content: 'https://a.com', title: 'A' },
{ id: 2, content: 'https://b.com', title: 'B' },
]
function BatchQR() {
const { getItemPreview, exportAll, isExporting, progress } =
useBatchBeautify({ items, dimensions: { width: 200, height: 200 } })
return (
<div>
{items.map((item, i) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const handle = getItemPreview(i)
useEffect(() => {
if (canvasRef.current) handle.drawPreview(canvasRef.current)
}, [handle])
return (
<div key={item.id}>
<canvas ref={canvasRef} style={{ width: 200, height: 200 }} />
<div>{item.title}</div>
</div>
)
})}
<button onClick={() => exportAll(3)} disabled={isExporting}>
{isExporting ? `导出中 ${Math.round(progress * 100)}%` : '批量导出 3×'}
</button>
</div>
)
}computePreviewRect
function computePreviewRect(
width: number,
height: number,
settings?: BeautificationSettings
): PreviewRect计算 QR 码在容器内的绘制区域,保证 QR 不越界,文字带不覆盖码体。
返回 PreviewRect
| 属性 | 说明 |
|------|------|
| qrSize | QR 区域的边长(像素),取 min(width, qrAreaHeight)。 |
| qrX | QR 区域左上角 X(水平居中:(width - qrSize) / 2)。 |
| qrY | QR 区域左上角 Y(扣除 above 文字带高度)。 |
| width | 透传容器宽度。 |
| height | 透传容器高度。 |
文字带逻辑:
- 若
settings.textOverlays含slot: 'above'且content非空,顶部预留fontSize + 2×padding + textQrGap高度,qrY偏移到该带之后。 - 若含
slot: 'below'且content非空,底部预留相同高度,qrSize相应缩小。 slot: 'middle'、position定位的项不影响布局。
PluginRegistry
class PluginRegistry {
register(plugin: BeautifyQRCodePlugin): void
unregister(pluginId: string): void
getPlugins(): BeautifyQRCodePlugin[]
applyTransformOptions(options: QROptions, settings: BeautificationSettings): Options
}| 方法 | 说明 |
|------|------|
| register(plugin) | 注册插件;同一 id 重复注册抛 Error。 |
| unregister(pluginId) | 按 id 移除插件,不存在时静默。 |
| getPlugins() | 返回当前所有已注册插件的数组(按注册顺序)。 |
| applyTransformOptions(options, settings) | 依次对每个插件调用 transformOptions,链式传递,返回最终 options。 |
使用时机:
- 需要运行时动态增删插件时用
pluginRegistry; - 仅使用固定插件列表时,直接传
plugins即可(Hook 内部自动创建 registry); - 两者同时传入时,
pluginRegistry优先,plugins被忽略。
PreviewArea
<PreviewArea
ref={ref}
width={300}
height={300}
mode="canvas" // 'canvas' | 'svg',仅写入 data-preview-mode,不影响行为
className="..."
style={{ ... }}
/>Ref 转发的 <div>,定位为 position: relative; overflow: hidden,作为预览挂载点。不内置绘制逻辑,需业务侧通过 ref 调用 drawPreview(canvas) 或 renderPreviewToSvg(ref.current)。
buildStylingOptions
function buildStylingOptions(
content: string,
settings: BeautificationSettings,
width: number,
height: number,
type?: 'canvas' | 'svg' // 默认 'canvas'
): Partial<Options> // qr-code-styling 的 Options将 content + BeautificationSettings + 尺寸转为 qr-code-styling 的原始 Options,不执行插件 transformOptions。
与 getOptionsForQR 的区别:
| | buildStylingOptions | getOptionsForQR |
|---|---|---|
| 插件 transformOptions | ❌ 不执行 | ✅ 链式执行 |
| 强制 width/height | ❌ 取传入值 | ✅ 强制为 qrSize |
| 返回类型 | Partial<Options> | Options |
适用于需要直接操作底层 options 或自定义渲染流程的场景。
Core API(非 React)
以下函数不依赖 React,可在组件外、Worker、或服务端(仅浏览器 API 可用时)使用。
getOptionsForQR
function getOptionsForQR(
content: string,
settings: BeautificationSettings,
qrSize: number,
type: 'canvas' | 'svg',
pluginRegistry?: PluginRegistry | null
): Options构建完整的 qr-code-styling Options:
- 调用
buildStylingOptions生成基础 options; - 若传入
pluginRegistry,依次执行各插件的transformOptions; - 强制
width = height = qrSize(防止插件修改尺寸导致越界)。
createQRInstance
function createQRInstance(
content: string,
settings: BeautificationSettings,
qrSize: number,
type: 'canvas' | 'svg',
extensions: ExtensionFunction[],
pluginRegistry?: PluginRegistry | null
): QRCodeStyling创建并返回已应用 extensions(SVG 扩展函数)的 QRCodeStyling 实例,可调用 .append(container) 挂载到 DOM,或 .getRawData(extension) 取 Blob。
getQRRawData
async function getQRRawData(
content: string,
settings: BeautificationSettings,
qrSize: number,
extension: 'png' | 'jpeg' | 'webp' | 'svg',
extensions: ExtensionFunction[],
pluginRegistry?: PluginRegistry | null
): Promise<Blob | null>直接生成指定格式的 Blob(不经过 Hook 渲染管线,不含插件 beforeRender/afterRender)。适用于服务端批处理、Worker 或不需要 canvas 插件的场景。
getQRRawDataForCanvas
async function getQRRawDataForCanvas(
content: string,
settings: BeautificationSettings,
cssQrSize: number,
physicalScale: number,
extensions: ExtensionFunction[],
pluginRegistry?: PluginRegistry | null
): Promise<Blob | null>专为高 DPR Canvas 渲染设计的 PNG 生成器,解决 getQRRawData 直接放大时像素比例失真的问题:
- 先在 CSS 像素尺寸(
cssQrSize)下解析所有 options(含插件 transformOptions),确保插件始终看到与 SVG 模式一致的 CSS 像素值; - 再将像素型字段(
margin、imageOptions.margin)按physicalScale等比放大,width/height设为cssQrSize × physicalScale; - 比例型字段(
imageSize、backgroundRound等)保持不变。
physicalScale <= 1 时退化为 getQRRawData。
downloadURI
function downloadURI(uri: string, name: string): void通过创建隐藏 <a download> 触发浏览器下载。uri 可为 Data URL 或 Blob URL;name 为文件名(含扩展名)。
内置插件
backgroundImagePlugin
在 QR 码下方渲染自定义背景图。
配置:通过 settings.pluginOptions['background-image'] 传入 BackgroundImageOptions:
| 属性 | 类型 | 默认 | 说明 |
|------|------|------|------|
| imageUrl | string | — | Data URL 或 Blob URL(必须是同域或 Data URL) |
| opacity | number | 1 | 不透明度 0–1 |
| position | 'fill' \| 'fit' \| 'stretch' | 'fill' | fill=裁切填满;fit=等比缩放居中留空;stretch=拉伸不保比 |
工作机制:
transformOptions:当imageUrl存在时,自动将 QR 背景色设为transparent。beforeRender(Canvas):异步加载图片(带内存缓存),在绘制 QR 前将背景图 draw 到 canvas,自动处理 DPR 坐标(从canvas.dataset.cssWidth/Height读取 CSS 尺寸)。svgExtension(SVG):在 SVG 根节点最前面插入<image>元素,preserveAspectRatio按position映射为对应值。
import { useBeautifyQRCode, backgroundImagePlugin } from '@gulibs/beautify-qrcode-hooks'
import type { BackgroundImageOptions, BeautificationSettings } from '@gulibs/beautify-qrcode-hooks'
const settings = useMemo<BeautificationSettings>(() => ({
qrCodeStyle: { backgroundColor: 'transparent', margin: 8 },
pluginOptions: {
'background-image': {
imageUrl: myBgUrl,
opacity: 0.7,
position: 'fill',
} satisfies BackgroundImageOptions,
},
}), [myBgUrl])
const { drawPreview } = useBeautifyQRCode({
content: 'https://example.com',
dimensions: { width: 300, height: 300 },
plugins: [backgroundImagePlugin],
settings,
})loadBackgroundImage(url):独立导出的工具函数,返回 Promise<HTMLImageElement>,带 URL→HTMLImageElement 内存缓存,可用于预加载。
textOverlayPlugin
在 QR 码上方/下方/指定位置渲染文字,Canvas 与 SVG 双模式同步。
配置:通过 settings.textOverlays: TextOverlayItem[] 传入,无需 pluginOptions。
TextOverlayItem
| 属性 | 类型 | 默认 | 说明 |
|------|------|------|------|
| id | string | 必填 | 唯一标识。 |
| content | string | 必填 | 文字内容,空字符串不渲染。 |
| slot | 'above' \| 'middle' \| 'below' | — | 预设槽位。above/below 配合 computePreviewRect 预留空间;middle 居中于 QR 区域。slot 优先于 position。 |
| position | { x: number; y: number } | { x: 0.5, y: 0.9 } | 归一化坐标(0=左/上,1=右/下),无 slot 时生效,映射为 x × width, y × height。 |
| positionOffset | { x: number; y: number } | — | 在 slot/position 基础上的附加归一化偏移(目前保留字段,插件内部暂未应用)。 |
| style | TextOverlayStyle | — | 文字样式,见下表。 |
TextOverlayStyle
| 属性 | 类型 | 默认 | 说明 |
|------|------|------|------|
| fontSize | number | 16 | 字号(px) |
| fontFamily | string | 'Arial' | 字体族 |
| color | string | '#000000' | 文字颜色 |
| fontWeight | string | 'normal' | 字重,如 'bold'、'600' |
| alignment | 'left' \| 'center' \| 'right' | 'center' | 水平对齐 |
| strokeWidth | number | — | 描边宽度(px);设置后同时绘制描边 |
| strokeColor | string | '#ffffff' | 描边颜色 |
| shadowOffsetX | number | 0 | 阴影 X 偏移 |
| shadowOffsetY | number | 0 | 阴影 Y 偏移 |
| shadowBlur | number | — | 阴影模糊半径;> 0 时启用阴影 |
| shadowColor | string | 'rgba(0,0,0,0.5)' | 阴影颜色 |
| backgroundColor | string | — | 文字背景填充色(绘制在文字矩形后面) |
| borderWidth | number | — | 文字背景边框宽度;需同时设置 backgroundColor 或 borderColor |
| borderColor | string | '#000000' | 文字背景边框颜色 |
工作机制:
afterRender(Canvas):遍历settings.textOverlays,按 slot/position 计算坐标,逐条绘制文字(含背景、描边、阴影)。从canvas.dataset.cssWidth/Height读取 CSS 尺寸,DPR 无关。svgExtension(SVG):为每条文字生成对应的 SVG<text>元素,样式与 Canvas 渲染保持视觉一致。
import { useBeautifyQRCode, textOverlayPlugin } from '@gulibs/beautify-qrcode-hooks'
import type { BeautificationSettings } from '@gulibs/beautify-qrcode-hooks'
const settings = useMemo<BeautificationSettings>(() => ({
qrCodeStyle: { margin: 8 },
textOverlays: [
{
id: 'header',
content: '扫码关注',
slot: 'above',
style: { fontSize: 16, color: '#1e40af', fontWeight: 'bold' },
},
{
id: 'footer',
content: 'example.com',
slot: 'below',
style: { fontSize: 13, color: '#6b7280' },
},
],
}), [])
const { drawPreview, renderPreviewToSvg } = useBeautifyQRCode({
content: 'https://example.com',
dimensions: { width: 300, height: 360 }, // height > width 为文字带预留空间
plugins: [textOverlayPlugin],
settings,
})组合多个插件
多个插件按数组顺序执行,彼此独立:
const { drawPreview } = useBeautifyQRCode({
content: 'https://example.com',
dimensions: { width: 300, height: 360 },
plugins: [backgroundImagePlugin, textOverlayPlugin],
settings: useMemo(() => ({
qrCodeStyle: { backgroundColor: 'transparent', margin: 8 },
pluginOptions: {
'background-image': { imageUrl: bgUrl, opacity: 0.6, position: 'fill' },
},
textOverlays: [
{ id: 'footer', content: 'Scan Me', slot: 'below', style: { fontSize: 14 } },
],
}), [bgUrl]),
})插件开发
插件只负责渲染逻辑(修改 QR options、修改 SVG、在 canvas 上绘制);UI 与状态由业务侧管理,通过 settings.pluginOptions[id] 与插件通信。
BeautifyQRCodePlugin 接口
interface BeautifyQRCodePlugin {
id: string
transformOptions?: (options: QROptions, settings: BeautificationSettings) => QROptions
svgExtension?: ExtensionFunction | PluginSvgExtension
svgExtensionOnlyForSvgOutput?: boolean
beforeRender?: (
ctx: CanvasRenderingContext2D,
settings: BeautificationSettings,
canvas: HTMLCanvasElement
) => void | Promise<void>
afterRender?: (
ctx: CanvasRenderingContext2D,
settings: BeautificationSettings,
canvas: HTMLCanvasElement
) => void | Promise<void>
}| 字段 | 说明 |
|------|------|
| id | 唯一字符串,作为 settings.pluginOptions[id] 的键,以及 PluginRegistry 的索引。 |
| transformOptions(options, settings) | 在生成 QR 前修改底层 qr-code-styling Options(如增加 margin、修改颜色)。链式执行,必须返回新 options 对象(不可 mutate)。不论 Canvas 还是 SVG 模式均会调用。 |
| svgExtension(svg, options[, settings]) | 仅在有 SVG 输出时调用(SVG 预览、SVG 导出;以及 svgExtensionOnlyForSvgOutput 为 false 时的栅格导出)。操作 SVG 根节点,追加/修改 SVG 元素。options.width/height 为外层容器尺寸(见坐标系一致性章节)。 |
| svgExtensionOnlyForSvgOutput | 默认 false。设为 true 后,svgExtension 仅在 SVG 输出时调用,栅格(PNG/JPEG/WebP)导出时跳过。通常与同时实现 afterRender 的插件配合,避免同一效果被渲染两次。 |
| beforeRender(ctx, settings, canvas) | Canvas 绘制前调用(在生成 QR 之前)。支持 async,Hook 会 await。典型用途:绘制背景图、渐变背景。 |
| afterRender(ctx, settings, canvas) | Canvas 绘制后调用(在 QR 绘制到 canvas 之后)。支持 async,Hook 会 await。典型用途:绘制文字、水印、边框装饰。 |
执行顺序
Canvas 预览 / 栅格导出(PNG, JPEG, WebP):
transformOptions(若用 registry)
→ beforeRender × N(按 plugins 数组顺序,await async)
→ 生成 QR 图像(getQRRawDataForCanvas)
→ 将 QR drawImage 到 canvas
→ afterRender × N(按 plugins 数组顺序,await async)
SVG 预览 / SVG 导出:
transformOptions(若用 registry)
→ 生成 QR SVG blob
→ svgExtension × N(按 plugins 数组顺序;skipped if svgExtensionOnlyForSvgOutput && 当前是栅格路径)Canvas DPR 注意事项
在 beforeRender / afterRender 中,canvas buffer 尺寸为物理像素(CSS 尺寸 × DPR),但 ctx 已 scale(dpr, dpr),所以绘制坐标使用 CSS 像素即可:
// ✅ 正确:从 dataset 读取 CSS 尺寸
const w = Number(canvas.dataset.cssWidth) || canvas.width
const h = Number(canvas.dataset.cssHeight) || canvas.height
ctx.fillText('hello', w / 2, h - 10) // CSS 像素坐标
// ❌ 错误:直接用 canvas.width 是物理像素,DPR=2 时坐标翻倍偏移
ctx.fillText('hello', canvas.width / 2, canvas.height - 10)插件状态读写
// 业务侧(组件)写入状态
updateSettings(prev => ({
...prev,
pluginOptions: {
...prev.pluginOptions,
[myPluginId]: { text: '© 2026', color: '#999' },
},
}))
// 插件内读取状态
interface MyOptions { text?: string; color?: string }
const plugin: BeautifyQRCodePlugin = {
id: myPluginId,
afterRender(ctx, settings, canvas) {
const opts = settings.pluginOptions?.[myPluginId] as MyOptions | undefined
if (!opts?.text) return
// ...绘制
},
}自定义插件完整示例
import type { BeautifyQRCodePlugin } from '@gulibs/beautify-qrcode-hooks'
interface WatermarkOptions {
text?: string
color?: string
fontSize?: number
}
/**
* 水印插件:在 canvas 右下角 afterRender 绘制,SVG 模式通过 svgExtension 添加同等效果。
* svgExtensionOnlyForSvgOutput: true — 避免 PNG 导出时 svgExtension 与 afterRender 重复绘制。
*/
export const watermarkPlugin: BeautifyQRCodePlugin = {
id: 'watermark',
svgExtensionOnlyForSvgOutput: true,
afterRender(ctx, settings, canvas) {
const opts = settings.pluginOptions?.['watermark'] as WatermarkOptions | undefined
if (!opts?.text) return
const w = Number(canvas.dataset.cssWidth) || canvas.width
const h = Number(canvas.dataset.cssHeight) || canvas.height
const fs = opts.fontSize ?? 12
ctx.save()
ctx.font = `${fs}px sans-serif`
ctx.fillStyle = opts.color ?? 'rgba(0,0,0,0.25)'
ctx.textAlign = 'right'
ctx.textBaseline = 'bottom'
ctx.fillText(opts.text, w - 6, h - 6)
ctx.restore()
},
svgExtension(svg, _options, settings) {
const opts = settings?.pluginOptions?.['watermark'] as WatermarkOptions | undefined
if (!opts?.text) return
const w = Number(svg.getAttribute('width')) || 300
const h = Number(svg.getAttribute('height')) || 300
const fs = opts.fontSize ?? 12
const ns = 'http://www.w3.org/2000/svg'
const el = document.createElementNS(ns, 'text')
el.setAttribute('x', String(w - 6))
el.setAttribute('y', String(h - 6))
el.setAttribute('text-anchor', 'end')
el.setAttribute('dominant-baseline', 'auto')
el.setAttribute('font-size', String(fs))
el.setAttribute('font-family', 'sans-serif')
el.setAttribute('fill', opts.color ?? 'rgba(0,0,0,0.25)')
el.textContent = opts.text
svg.appendChild(el)
},
}
// 使用方式:
// plugins={[watermarkPlugin]}
// settings={{ pluginOptions: { watermark: { text: '© 2026', color: '#888' } } }}Extension vs Plugin vs PluginRegistry 对比
| | extension | plugins | pluginRegistry |
|---|---|---|---|
| 传参位置 | extension: fn | plugins: [...] | pluginRegistry: registry |
| 支持 transformOptions | ❌ | ✅ | ✅ |
| 支持 svgExtension | ✅(即为 extension 本身) | ✅ | ✅ |
| 支持 beforeRender / afterRender | ❌ | ✅ | ✅ |
| 支持运行时增删 | ❌ | ❌(重传数组) | ✅ register/unregister |
| 接收 settings 参数 | ❌ | ✅ | ✅ |
| 适用场景 | 简单 SVG 装饰(边框、水印),无状态 | 有配置的功能插件,静态列表 | 动态插件管理,运行时按需启用 |
BeautificationSettings 完整类型参考
BeautificationSettings
interface BeautificationSettings {
qrCodeStyle?: QRCodeStyleSettings
logo?: LogoSettings
textOverlays?: TextOverlayItem[]
pluginOptions?: Record<string, unknown> // 插件状态,按 plugin.id 键
}QRCodeStyleSettings
| 属性 | 类型 | 默认 | 说明 |
|------|------|------|------|
| foregroundColor | string | '#000000' | 单色模式下码点颜色(dotsColorType: 'single' 时生效) |
| backgroundColor | string | '#FFFFFF' | 单色背景色(backgroundColorType: 'single' 时生效) |
| errorCorrectionLevel | 'L' \| 'M' \| 'Q' \| 'H' | 'H' | 纠错级别;有 logo 时建议 'Q' 或 'H' |
| margin | number | 0 | 外边距(px,CSS 尺寸空间),QR 图内部的静区宽度 |
| version | number | 自动 | QR 版本 1–40(点阵密度);若内容需要更高版本则自动升级 |
| mode | 'Numeric' \| 'Alphanumeric' \| 'Byte' \| 'Kanji' | 自动 | 编码模式;内容不符合该模式时自动降级 |
| dotType | DotType | 'square' | 码点形状:square dots rounded classy classy-rounded extra-rounded |
| cornerSquareType | CornerSquareType | 'square' | 角框(定位图案外框)形状:square dot extra-rounded rounded dots classy classy-rounded |
| cornerDotType | CornerDotType | 'square' | 角点(定位图案内点)形状:square dot rounded dots classy classy-rounded extra-rounded |
| shape | 'square' \| 'circle' | 'square' | QR 整体外形(circle 将码裁切为圆形) |
| dotsRoundSize | boolean | true | 是否对码点启用圆角对齐优化 |
| backgroundRound | number | — | 背景矩形圆角半径(0–1 比例) |
| dotsColorType | 'single' \| 'gradient' | 'single' | 码点着色方式;'gradient' 时读 foregroundGradient |
| foregroundGradient | ColorGradient | — | 码点渐变配置(dotsColorType: 'gradient' 时生效) |
| cornerSquareColorType | 'single' \| 'gradient' | 'single' | 角框着色方式 |
| cornerSquareColor | string | 同 foregroundColor | 角框单色颜色 |
| cornerSquareGradient | ColorGradient | — | 角框渐变 |
| cornerDotColorType | 'single' \| 'gradient' | 'single' | 角点着色方式 |
| cornerDotColor | string | 同 foregroundColor | 角点单色颜色 |
| cornerDotGradient | ColorGradient | — | 角点渐变 |
| backgroundColorType | 'single' \| 'gradient' | 'single' | 背景着色方式 |
| backgroundGradient | ColorGradient | — | 背景渐变 |
ColorGradient
interface ColorGradient {
type: 'linear' | 'radial'
rotation?: number // 旋转角度(度,非弧度)
colorStops: Array<{ offset: number; color: string }> // offset 0–1
}LogoSettings
| 属性 | 类型 | 默认 | 说明 |
|------|------|------|------|
| url | string | 必填 | logo 图片 URL;支持 Data URL(data:)和 HTTP URL |
| size | number | 40 | logo 占 QR 尺寸的百分比(1–100) |
| margin | number | 0 | logo 周围的清除区域宽度(px,输出图像空间) |
| hideBackgroundDots | boolean | true | 是否隐藏 logo 区域内的码点 |
| crossOrigin | 'anonymous' \| 'use-credentials' | 自动 | 跨域图片的 CORS 策略;HTTP URL 自动设为 'anonymous' |
| saveAsBlob | boolean | 自动 | Data URL 图片是否先转为 Blob 再加载(Data URL 时自动为 true) |
其他类型
| 类型 | 说明 |
|------|------|
| FileExtension | 'png' \| 'jpeg' \| 'webp' \| 'svg' |
| RasterExportFormat | 'png' \| 'jpeg' \| 'webp'(不含 svg) |
| ExtensionFunction | (svg: SVGElement, options: QROptions) => void |
| PluginSvgExtension | (svg: SVGElement, options: QROptions, settings?: BeautificationSettings) => void |
| PreviewRect | { qrSize, qrX, qrY, width, height } |
| QRCodeErrorContext | { source: 'canvasPreview' \| 'svgPreview' \| 'export' \| 'batch' } |
| QRCodeErrorCallback | (error: unknown, context: QRCodeErrorContext) => void |
| QRCodeStylingOptions | qr-code-styling 的原始 Options 类型(re-export) |
| BatchDisplayMode | 'grid' \| 'tabs'(保留,未在 Hook 内使用) |
完整导出列表
// Hooks
export { useBeautifyQRCode } from '...'
export type { UseBeautifyQRCodeOptions, UseBeautifyQRCodeReturn } from '...'
export { useBatchBeautify } from '...'
export type { UseBatchBeautifyOptions, UseBatchBeautifyReturn, ItemPreviewHandle } from '...'
// 工具函数
export { computePreviewRect } from '...'
export { PluginRegistry } from '...'
export { buildStylingOptions } from '...'
export { PreviewArea } from '...'
export type { PreviewAreaProps } from '...'
// 内置插件
export { backgroundImagePlugin, loadBackgroundImage } from '...'
export { textOverlayPlugin } from '...'
// Core(非 React)
export { getOptionsForQR, createQRInstance, getQRRawData, downloadURI } from '...'
// 类型
export type {
BeautificationSettings, QRCodeStyleSettings, LogoSettings,
TextOverlayItem, TextOverlayStyle, BackgroundImageOptions,
BeautifyQRCodePlugin, ExtensionFunction, PluginSvgExtension,
BatchBeautifyItem, BatchDisplayMode, FileExtension, RasterExportFormat,
PreviewRect, QRCodeErrorCallback, QRCodeErrorContext,
QRCodeStylingOptions, ColorGradient, ColorGradientType,
DotType, CornerSquareType, CornerDotType,
} from '...'与 qr-code-styling 官网能力对照
| 区域 | 选项 | 支持 | 映射字段 |
|------|------|------|------|
| 主选项 | Data | ✅ | content |
| | Width / Height | ✅ | dimensions |
| | Margin(静区) | ✅ | qrCodeStyle.margin |
| | Image(Logo) | ✅ | logo.url |
| Dots | 样式 | ✅ | qrCodeStyle.dotType |
| | 单色 | ✅ | qrCodeStyle.foregroundColor |
| | 渐变(含旋转) | ✅ | qrCodeStyle.foregroundGradient |
| Corners Square | 样式 | ✅ | qrCodeStyle.cornerSquareType |
| | 单色/渐变 | ✅ | qrCodeStyle.cornerSquareColor/Gradient |
| Corners Dot | 样式 | ✅ | qrCodeStyle.cornerDotType |
| | 单色/渐变 | ✅ | qrCodeStyle.cornerDotColor/Gradient |
| Background | 单色/渐变 | ✅ | qrCodeStyle.backgroundColor/Gradient |
| | 圆角 | ✅ | qrCodeStyle.backgroundRound |
| Background Image | 贴图 / 透明度 / 填充模式 | ✅ | backgroundImagePlugin + BackgroundImageOptions |
| Text Overlay | 自由位置/上下文字 | ✅ | textOverlayPlugin + TextOverlayItem[] |
| Image Options | Hide background dots | ✅ | logo.hideBackgroundDots |
| | Logo Size | ✅ | logo.size(百分比) |
| | Logo Margin | ✅ | logo.margin |
| QR Options | Type Number(版本) | ✅ | qrCodeStyle.version |
| | Error Correction Level | ✅ | qrCodeStyle.errorCorrectionLevel |
| | Mode | ✅ | qrCodeStyle.mode |
| | Shape(圆形 QR) | ✅ | qrCodeStyle.shape |
| Export | PNG / JPEG / WebP / SVG | ✅ | download / getRawData |
| | 自定义导出分辨率 | ✅ | scale 参数(默认 2×) |
| | 导出为 Data URL | ✅ | exportAsDataURL |
| | 配置序列化为 JSON | ✅ | JSON.stringify(settings) |
脚本
pnpm dev # 启动示例应用(Vite dev server,含完整功能演示)
pnpm build # 构建库到 dist/(ES + CJS + 类型声明)
pnpm lint # ESLint 检查