葵花码算法说明
本文描述当前实现实际执行的算法。编码以链接文字为输入,以放射状二值矩阵为中间结果,最后输出 SVG 或 PNG。全部计算在浏览器中完成。
1. 处理流程
链接校验
→ 选择字母数字模式或 UTF-8 字节模式
→ 写入模式、长度和载荷位
→ 选择射线版本与纠错等级
→ 终止位、字节补齐和填充
→ 分块并计算 Reed–Solomon 纠错码
→ 交织码字
→ 写入放射矩阵、掩码和格式信息
→ 对 32 种掩码评分,保留最佳结果
→ 回读矩阵,校验并还原原链接
→ 绘制 SVG,按需转换成 PNG
assets/codec.js 实现编码、回读和绘制;assets/protocol-data.js 保存格式字、数据位置和掩码常量;assets/app.js 处理输入、头像和导出。
2. 输入校验
先执行 trim() 去除两端空白,再检查以下条件:
- 输入不为空,以 HTTP 或 HTTPS 协议开头。
- 文本内部不包含空格、换行等空白字符。
- 可以通过浏览器
URL解析,并且具有主机名。 - JavaScript 字符串长度不超过 512 个 UTF-16 代码单元;这是预处理上限,不是编码容量。
URL 对象仅用于校验。编码使用去除两端空白后的原文本,不自动补斜杠、改写域名大小写、百分号编码、请求网页或生成短链接。
3. 载荷编码
3.1 两种模式
如果所有字符都属于下面的 82 字符集合,使用字母数字模式。字符顺序决定其数值,不能调整:
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$&'()*+,/:;=?@-._~
该集合记为 U。字母数字模式还使用一个 45 字符集合 A,其中 Z 后面包含一个空格:
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:
若包含中文、Emoji 或其他不在 U 中的字符,整个输入采用 UTF-8 字节模式。百分号 % 不在 U 中,因此包含 %20 等转义序列的输入也会进入字节模式。
3.2 流式进制转换
字母数字模式先将文本转换为 A 中的字符:
value = unpackRadix(packRadix(text, U), A)
这里使用流式转换,不能替换成“把整个字符串作为一个大整数再转进制”,否则结果不同。
packRadix(text, alphabet) 的步骤如下:
value = 0
leading = 0
started = false
bytes = []
逐个读取字符:
digit = 字符在 alphabet 中的下标
若尚未开始且 digit == 0:leading += 1,继续
started = true
value = value × alphabet.length + digit
当 value >= 256:
bytes 追加 value & 255
value = floor(value / 256)
若 value != 0:bytes 追加 value
若 leading > 255:拒绝输入
返回 reverse(bytes 后面追加 leading)
首字节保存前导零字符数量;后续字节保存流式转换结果。这里的“零字符”指字符集下标为 0 的字符,即 0。
unpackRadix(bytes, alphabet) 的步骤如下:
value = 0
digits = []
依次读取 bytes[1:]:
value = value × 256 + byte
当 value >= alphabet.length:
digits 追加 value % alphabet.length
value = floor(value / alphabet.length)
若 value != 0:digits 追加 value
再追加 bytes[0] 个零
反转 digits,将每个下标映射为 alphabet 中的字符
例如,packRadix("12", U) 得到 [0, 84];再用 A 解出 "1*"。实际链接也按相同规则转换。解码时交换 U 与 A,执行反向转换。
3.3 写入二进制字段
所有多位数值按高位在前写入,字节内部同样是最高位在前。
| 字段 | 字母数字模式 | UTF-8 字节模式 |
|---|---|---|
| 模式标识 | 4 位,数值 2,即 0010 | 4 位,数值 4,即 0100 |
| 长度 | 9 位,转换后的 A 字符数 | 8 位,UTF-8 字节数 |
| 内容 | 两字符一组写 11 位,剩一字符写 6 位 | 每个字节写 8 位 |
字母数字模式下,一对字符的值为 45 × a + b,a、b 是它们在 A 中的下标。最大值为 2024,11 位可容纳。最后单个字符最大值为 44,使用 6 位。
设转换后的字符数为 n,UTF-8 字节数为 m,则不含补齐部分的载荷位数为:
字母数字:B = 4 + 9 + 11 × floor(n / 2) + 6 × (n % 2)
UTF-8: B = 4 + 8 + 8 × m
这些模式借用了常见的位字段写法,但最终使用的是放射矩阵,并非标准 QR 方阵。
4. 容量与自动选择
内部版本 v = 0、1、2 分别对应界面中的 36、54、72 条射线。
每个配置用三元组 (D, P, K) 表示:D 为全部数据字节数,P 为每块纠错字节数,K 为块数。D 包含模式、长度、载荷、补齐和填充,不等于可输入的链接字节数。
| 射线 / 版本 | L:D / P / K | M:D / P / K | Q:D / P / K | H:D / P / K |
|---|---|---|---|---|
| 36 / 0 | 28 / 10 / 1 | 25 / 13 / 1 | 20 / 18 / 1 | 14 / 24 / 1 |
| 54 / 1 | 38 / 14 / 1 | 35 / 17 / 1 | 26 / 13 / 2 | 20 / 16 / 2 |
| 72 / 2 | 46 / 20 / 1 | 43 / 23 / 1 | 34 / 16 / 2 | 24 / 21 / 2 |
每个版本的总码字数 D + P × K 固定,分别为 38、52、66 字节,对应 304、416、528 个数据位置。
选择条件是 B <= 8D。射线为自动时,外层按 0、1、2 遍历;纠错为自动时,内层按 H、Q、M、L 遍历,使用第一个满足条件的配置。手动指定的选项只尝试该值,容量不足时不会擅自改动。
例如 https://example.com 的载荷为 134 位:36 条射线的 H 只有 112 位,装不下;Q 有 160 位,可以容纳,所以自动选择 36 条、Q 纠错,而不会继续选更多射线的 H。
5. 补齐数据字节
设当前位数为 B,容量为 8D:
- 追加
min(4, 8D − B)个零,作为终止位。 - 继续追加零,直到位数为 8 的整数倍。
- 每 8 位合并成一个字节。
- 数据不足 D 字节时,交替追加
0xEC、0x11,直到恰好为 D。
若原始载荷恰好用满容量,则不追加终止位。长度字段用于回读时确定载荷边界。
6. Reed–Solomon 纠错
6.1 有限域
纠错运算在 GF(256) 上进行,使用本原多项式:
p(x) = x^8 + x^4 + x^3 + x^2 + 1
十六进制表示:0x11D
本原元:α = 2
域中的加法和减法都为按位异或。乘法通过指数表 exp 和对数表 log 计算:
若 a == 0 或 b == 0:mul(a, b) = 0
否则:mul(a, b) = exp[log[a] + log[b]]
exp[0..254] 依次保存 α 的幂;exp[255..511] 以 255 为周期重复,避免乘法时另做取模。
6.2 分块与生成多项式
将 D 个数据字节按顺序等分成 K 块,每块 D/K 字节。当前配置都可以整除。
每块追加 P 个纠错字节。生成多项式为:
g(x) = ∏ (x + α^i),i = 0..P−1
系数按从高次到低次保存。先把数据后面补 P 个零,再逐个数据位置消元:
work = data + P 个零
for i = 0..data.length−1:
factor = work[i]
for j = 1..P:
work[i+j] ^= mul(g[j], factor)
完整块 = 原数据 + work 的最后 P 个字节
输出保留原数据,纠错字节为多项式除法的余数,因此是系统码。
6.3 交织
依次取各块的第 0 个字节,再取各块第 1 个字节,直到所有字节输出完毕。例如两块 A、B 的结果为:
A0, B0, A1, B1, …, A_last, B_last
交织后,全部数据字节在前,全部纠错字节在后;每一部分内部按块交替。
对于实现了完整纠错解码的接收端,每块 P 个纠错字节理论上可纠正最多 floor(P/2) 个未知位置的错误字节,或满足 2e + s <= P 的错误与已知位置擦除组合。该界限不能直接换算为图片可遮挡面积。当前回读只计算校验综合值,发现异常即拒绝,不实现受损码纠正。
7. 放射矩阵与固定表
7.1 逻辑坐标
矩阵固定为 72 × 16,下标为 matrix[ray][ring]。每个单元可能是 0、1 或 null:
- 0:白色数据位或格式位。
- 1:黑色数据位或格式位。
- null:该版本未使用的位置。
72 个候选方向之间相差 5°。36 和 54 条版本通过固定表选择部分射线承载数据;定位图形和格式信息还会占用或调整相应位置,不能仅靠肉眼数黑线判断版本。
7.2 数据位置表
protocol-data.js 的 data 有 3 行,每行 72 个整数。data[v][ray] 是一个 16 位位图:第 ring 位为 1,表示该位置用于写入交织码字。
按 ray 从 0 到 71、ring 从 0 到 15 扫描位图,将位置依次写入列表。第 i 个位置的数据位为:
bit = (codeword[i >> 3] >> (7 − i % 8)) & 1
三版本位置数量分别为 304、416、528,与总码字数相符。位置表同时为格式信息和定位区域留空。
7.3 掩码表
masks 有 32 行,每行 72 个 16 位整数。第 m 个掩码在某位置的值为:
maskBit = (masks[m][ray] >> ring) & 1
matrix[ray][ring] = bit XOR maskBit
掩码的作用是改变黑白分布,回读时再次异或同样的掩码即可还原。掩码不加密数据,也不改变数据容量。
7.4 格式信息
纠错等级的格式标识为 M=0、L=1、H=2、Q=3。格式表 formats 有 384 项,即 3 × 4 × 32 种配置,每项是一个 18 位格式字。
index = (version × 4 + levelBits) × 32 + mask
format = formats[index]
对所有奇数 ray(1、3、…、71),在 ring=6 写入:
matrix[ray][6] = (format >> (17 − ((ray−1)/2) % 18)) & 1
这将 18 位格式字按高位在前写入两次。当前实现查固定表,不在运行时推导格式表或掩码表。精确复现需要保留这三组常量及其顺序;本文件中的公式配合 protocol-data.js 才构成完整定义。
7.5 射线端部延伸
完成数据和格式写入后,对当前版本的有效射线延伸端部:
- 36 条版本处理奇数 ray;54 条版本处理不能被 4 整除的 ray;72 条版本处理全部 ray。
- 在 ring≥3 的范围找到第一个非 null 单元,把 ring=3 到它之前的位置填为该单元的值。
- 如果 ring=14 非 null,将同样的值复制到 ring=15。
这些是绘图延伸位,不增加编码容量,也不作为新的载荷位回读。
8. 掩码评分
每个输入尝试全部 32 个掩码。对完成格式位和端部延伸的矩阵逐行扫描,统计:
- dark:所有非 null 单元中,1 的数量。
- total:所有非 null 单元数量。
- joins:同一射线上,每个连续黑色段从第 2 个单元开始,每多一个单元加 1。
- long:每个连续黑色段从第 8 个单元开始,每多一个单元加 1。
score = joins − 4 × long − 150 × abs(dark / total − 0.5)
较高得分倾向于保留适度连续线段,减少过长黑线,并使黑白比例接近一半。选择分数最高者;分数相同则保留编号较小、先遍历到的掩码。该评分针对放射状图案,并非标准 QR 的掩码评分规则。
9. 回读校验
生成图片前,从刚生成的逻辑矩阵恢复文本:
- 从 ray=1、3、…、35 的 ring=6 读取第一份 18 位格式字,查格式表得到版本、等级和掩码;确认版本与调用参数一致。
- 按数据位置表读取矩阵,对每位异或掩码,还原交织码字。
- 反交织为 K 块,每块前
D/K字节为数据,后 P 字节为纠错。 - 对每块计算
S_i = C(α^i),i 从 0 到 P−1。只要一个综合值非零,就拒绝该结果。 - 按块顺序拼回数据,读取模式和长度。字母数字模式解出 A 字符后反向进制转换;字节模式用严格 UTF-8 解码。
- 将回读字符串与去除两端空白后的输入逐字比较。不一致时停止生成。
当前回读直接接受矩阵和已知版本,不从 PNG/SVG 提取像素。它只读取第一份格式信息,不利用第二份进行投票恢复,也不校验图像中的头像、定位圆或端部延伸。它用于检查编码数据是否完整,不能替代实际扫码器的图像定位、透视校正和识别。
10. SVG 几何绘制
10.1 坐标转换
SVG 使用 viewBox="0 0 1000 1000",中心为 (500, 500)。候选方向 ray、径向位置 ring 对应:
θ = (180 + 5 × ray) × π / 180
r = 250 + (ring − 3) × 1000 / 54
x = 500 + r × cos(θ)
y = 500 + r × sin(θ)
坐标保留三位小数。图像 y 轴朝下。只绘制 ring=3..15,ring=16 用于扫描时结束最后一个连续段。
10.2 圆点与线段
沿一条射线查找连续的 1:
- 单个 1 绘制半径 9 的圆点。
- 连续多个 1,从第一个位置连到最后一个位置,线宽 18,端点为圆头。
- 0 或 null 都会中断连续段。
数据图案颜色为 #080908,背景为白色。随后依次覆盖定位圆、中心图案和右下角标识。
10.3 固定图形与头像
三个定位圆中心分别为 (204,204)、(796,204)、(204,796);由半径 47 的黑圆、32 的白圆及 9 的黑圆同心叠加。
默认中心为 (500,500)、半径 224 的 #444444 圆,白色标识放大 2.2 倍。右下角为 (800,800)、半径 100 的 #2eac4b 圆,白色标识使用原尺寸。标识路径如下,采用无填充、白色描边、线宽 16、圆头及圆角连接:
M30-2C48-7 54-20 48-33C42-47 24-51 12-43
C4-38 0-31 0-22V22C0 31-4 38-12 43
C-24 51-42 47-48 33C-54 20-48 7-30 2
上传头像后,以图片替换默认中心,右下角标识保留。先从原图中心裁取边长为 min(width,height) 的正方形,缩放为 512 × 512 PNG;再嵌入 SVG 的 (276,276)、448 × 448 区域,并以半径 224 的圆裁剪。头像不进入编码载荷,所以不会改变射线数据。
11. 导出和交互状态
SVG 的宽高设置为 512、1024 或 2048,内部坐标始终为 1000 × 1000。PNG 导出时,将 SVG 加载为图片,绘制到指定像素尺寸的 Canvas,再调用 toBlob("image/png")。复制图片通过 Clipboard API 写入相同的 PNG Blob。
SVG 内嵌头像的 PNG 数据,不依赖外部图片请求。下载后缩放 SVG,数据线条仍是矢量,头像清晰度受位图分辨率限制。
渲染和头像读取都是异步操作。界面使用递增的版本号标记任务,旧任务完成时若版本号不匹配,就丢弃结果,避免快速修改输入后导出旧图片。修改链接和编码参数会使当前结果失效;修改头像或尺寸只重新绘制已有编码。
12. 完整计算示例
输入 https://example.com,射线和纠错均为自动:
| 项目 | 结果 |
|---|---|
| 载荷模式 | 字母数字模式,转换后 22 个字符 |
| 载荷位数 | 4 + 9 + 11 × 11 = 134 位 |
| 版本 / 射线 | 0 / 36 条 |
| 纠错等级 | Q |
| 数据 / 纠错 / 块数 | D=20、P=18、K=1 |
| 补齐 | 4 个终止零 + 6 个对齐零 + 2 个填充字节 |
| 总码字数 | 38 字节,即 304 位 |
| 最佳掩码 | 6 |
| 格式表下标 | (0 × 4 + 3) × 32 + 6 = 102 |
| 格式字 | 120809,即 18 位二进制 011101011111101001 |
| 评分 | 约 134.27014218009478 |
20 个数据字节,十六进制表示:
20 b0 f7 cf 9c 11 67 06 c6 03 72 16 6d 06 88 89 98 00 ec 11
18 个纠错字节:
19 0f cc f9 bc da ad 42 b9 8e f8 4f 76 45 99 36 18 44
只有一个分块,因此交织结果就是上述两段依次拼接。写入矩阵、加掩码和格式位后,回读结果必须严格等于 https://example.com。
13. 复杂度与复用接口
输入长度为 n,载荷编码为 O(n)。每块数据长度为 k、纠错长度为 P,直接构造生成多项式为 O(P²),计算余数为 O(kP)。32 次掩码评估各遍历固定的 72 × 16 矩阵,约为 O(32 × 72 × 16);PNG 栅格化的像素处理量随边长 S 按 O(S²) 增长。
页面依次加载 protocol-data.js 与 codec.js 后,可调用:
const result = SunflowerCode.encode('https://example.com', {
version: 'auto', // 或 0、1、2
level: 'auto' // 或 'L'、'M'、'Q'、'H'
});
const text = SunflowerCode.decode(result.matrix, result.version);
const svg = SunflowerCode.renderSvg(result, { size: 1024, avatar: '' });
encode 成功时返回版本、等级、掩码、矩阵、评分、输入文本、载荷位数、数据容量和交织码字;失败时抛出错误。avatar 必须为空或本地 PNG/JPEG/WebP 的 Base64 data URL。图像识别、目标网页访问与受损码修复不属于这三个接口的功能。