葵花码使用说明
← 返回生成器

葵花码算法说明

本文描述当前实现实际执行的算法。编码以链接文字为输入,以放射状二值矩阵为中间结果,最后输出 SVG 或 PNG。全部计算在浏览器中完成。

1. 处理流程


链接校验

  → 选择字母数字模式或 UTF-8 字节模式

  → 写入模式、长度和载荷位

  → 选择射线版本与纠错等级

  → 终止位、字节补齐和填充

  → 分块并计算 Reed–Solomon 纠错码

  → 交织码字

  → 写入放射矩阵、掩码和格式信息

  → 对 32 种掩码评分,保留最佳结果

  → 回读矩阵,校验并还原原链接

  → 绘制 SVG,按需转换成 PNG

assets/codec.js 实现编码、回读和绘制;assets/protocol-data.js 保存格式字、数据位置和掩码常量;assets/app.js 处理输入、头像和导出。

2. 输入校验

先执行 trim() 去除两端空白,再检查以下条件:

  1. 输入不为空,以 HTTP 或 HTTPS 协议开头。
  2. 文本内部不包含空格、换行等空白字符。
  3. 可以通过浏览器 URL 解析,并且具有主机名。
  4. 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,即 00104 位,数值 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 / KM:D / P / KQ:D / P / KH:D / P / K
36 / 028 / 10 / 125 / 13 / 120 / 18 / 114 / 24 / 1
54 / 138 / 14 / 135 / 17 / 126 / 13 / 220 / 16 / 2
72 / 246 / 20 / 143 / 23 / 134 / 16 / 224 / 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:

  1. 追加 min(4, 8D − B) 个零,作为终止位。
  2. 继续追加零,直到位数为 8 的整数倍。
  3. 每 8 位合并成一个字节。
  4. 数据不足 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:

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 射线端部延伸

完成数据和格式写入后,对当前版本的有效射线延伸端部:

这些是绘图延伸位,不增加编码容量,也不作为新的载荷位回读。

8. 掩码评分

每个输入尝试全部 32 个掩码。对完成格式位和端部延伸的矩阵逐行扫描,统计:


score = joins − 4 × long − 150 × abs(dark / total − 0.5)

较高得分倾向于保留适度连续线段,减少过长黑线,并使黑白比例接近一半。选择分数最高者;分数相同则保留编号较小、先遍历到的掩码。该评分针对放射状图案,并非标准 QR 的掩码评分规则。

9. 回读校验

生成图片前,从刚生成的逻辑矩阵恢复文本:

  1. 从 ray=1、3、…、35 的 ring=6 读取第一份 18 位格式字,查格式表得到版本、等级和掩码;确认版本与调用参数一致。
  2. 按数据位置表读取矩阵,对每位异或掩码,还原交织码字。
  3. 反交织为 K 块,每块前 D/K 字节为数据,后 P 字节为纠错。
  4. 对每块计算 S_i = C(α^i),i 从 0 到 P−1。只要一个综合值非零,就拒绝该结果。
  5. 按块顺序拼回数据,读取模式和长度。字母数字模式解出 A 字符后反向进制转换;字节模式用严格 UTF-8 解码。
  6. 将回读字符串与去除两端空白后的输入逐字比较。不一致时停止生成。

当前回读直接接受矩阵和已知版本,不从 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:

数据图案颜色为 #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。图像识别、目标网页访问与受损码修复不属于这三个接口的功能。