DeepSeek写的设计文档:给CnQRCode增加二维码解码功能
概述
本文档描述在现有 Delphi 5 项目 CnQRCode.pas 中新增二维码解码功能的完整技术设计。解码功能分三个阶段实现,参考 zxing 开源库的算法,严格遵循 Delphi 5 语法规范和 CnPack 编码约定。
设计目标
• 在现有 TCnQREncoder类的同一单元内新增解码能力• 阶段一:从 TCnQRData(二维 0/1 矩阵)直接解码出文本• 阶段二:从 TBitmap图像经二值化、图案定位、透视变换得到TCnQRData• 阶段三:串联阶段一和阶段二,提供端到端接口
技术约束
• Delphi 5 语法,兼容 C++Builder • uses单元名无前缀(Graphics而非Vcl.Graphics)• interface部分类型以TCn开头,整型常量以CN_开头,字符串常量以SCN_开头• record不支持方法,不使用匿名函数,类里不支持const和type• 不使用带赋值的枚举类型语法,用独立常量代替 • 不支持内联 var,局部变量在函数头部显式声明• 不用 Exit(True),写成Result := True; Exit;两句• 类的数组成员变量不使用 property(兼容 C++Builder)• 动态字节数组使用 TBytes• 不用辅助工具类集合函数,用固定前缀的独立函数( CnQRXxx)
架构
三阶段模块划分
┌─────────────────────────────────────────────────────────────────┐
│ 阶段三:端到端接口 │
│ CnQRDecodeFromBitmap(ABitmap, AText) │
│ CnQRDecodeFromMatrix(AMatrix, AText) │
└──────────────────────┬──────────────────────────────────────────┘
│
┌────────────┴────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────────────────────┐
│ 阶段一:矩阵解码 │ │ 阶段二:图像检测 │
│ TCnQRDecoder 类 │ │ 独立函数组(CnQR 前缀) │
│ │ │ │
│ ReadFormatInfo │ │ CnQRBinarize │
│ ReadVersion │ │ CnQRFindFinderPatterns │
│ UnmaskMatrix │ │ CnQRFindAlignmentPattern │
│ ReadCodewords │ │ CnQRCalcPerspectiveTransform │
│ SplitDataBlocks │ │ CnQRTransformPoint │
│ RSDecodeBlock │ │ CnQRSampleGrid │
│ DecodeDataStream │ │ CnQRCalcModuleSize │
└─────────────────────┘ └─────────────────────────────────────┘
▲ │
│ ▼
│ ┌─────────────────────┐
└───────────────│ TCnQRData 矩阵 │
└─────────────────────┘数据流
TBitmap
│
▼ CnQRBinarize(灰度转换 + 自适应阈值)
TCnQRData(二值矩阵,原始像素坐标)
│
▼ CnQRFindFinderPatterns(行扫描状态机 + 四重验证)
TCnQRFinderPattern[3](三个寻像图案中心)
│
▼ CnQRFindAlignmentPattern(局部搜索 + 交叉验证)
TCnQRAlignmentPattern(可选,版本2+)
│
▼ CnQRCalcPerspectiveTransform(四点映射)
TCnQRPerspectiveTransform(3x3 变换矩阵)
│
▼ CnQRSampleGrid(逆变换采样)
TCnQRData(规范化模块矩阵,Dimension x Dimension)
│
▼ TCnQRDecoder.DecodeMatrix
├─ ReadFormatInformation(读取格式信息,汉明距离匹配)
├─ ReadVersion(读取版本信息)
├─ UnmaskMatrix(去掩码)
├─ ReadCodewords(Z字形路径读取)
├─ SplitDataBlocks(交织解交织)
├─ RSDecodeBlock × N(Berlekamp-Massey RS 纠错)
└─ DecodeDataStream(比特流解析)
│
▼
string(解码文本)类型定义
本章定义解码功能新增的所有类型。注释遵循 CnPack 惯例,放置在注释对象的下一行。
核心解码类型
type
TCnQRFormatInfo = record
ErrorLevel: TCnErrorRecoveryLevel;
MaskType: Integer;
end;
{* QR码格式信息,包含纠错级别和掩码类型(0..7)}
TCnQRFinderPattern = record
X: Double;
Y: Double;
EstimatedModuleSize: Double;
ConfirmedCount: Integer;
end;
{* 寻像图案记录,包含中心坐标、估算模块尺寸和确认计数}
TCnQRAlignmentPattern = record
X: Double;
Y: Double;
EstimatedModuleSize: Double;
end;
{* 对齐图案记录,包含中心坐标和估算模块尺寸}
TCnQRPerspectiveTransform = record
a11, a12, a13: Single;
a21, a22, a23: Single;
a31, a32, a33: Single;
end;
{* 3x3 透视变换矩阵(单精度浮点数),用于四边形到四边形映射}
TCnQRFinderPatternInfo = record
TopLeft: TCnQRFinderPattern;
TopRight: TCnQRFinderPattern;
BottomLeft: TCnQRFinderPattern;
end;
{* 三个寻像图案的信息记录,含左上、右上、左下坐标}
TCnQRDecodeMode = (
qrmTerminator,
qrmNumeric,
qrmAlphaNumeric,
qrmStructuredAppend,
qrmByte,
qrmECI,
qrmKanji,
qrmFNC1First,
qrmFNC1Second,
qrmHanzi
);
{* 解码模式枚举,对应 QR 码规范中 4 位模式指示符的各种编码模式}
TCnQRBinarizeResult = record
Matrix: TCnQRData;
Width: Integer;
Height: Integer;
end;
{* 二值化结果,包含二值矩阵及其宽高}TCnQRDecoder 类
TCnQRDecoder = class
private
FQRData: TCnQRData;
FQRSize: Integer;
FFormatInfo: TCnQRFormatInfo;
FQRVersion: TCnQRCodeVersion;
FIsMirrored: Boolean;
FErrorMessage: string;
FLastError: string;
// ===== 格式信息解码 =====
function ReadFormatInformation: Boolean;
function ExtractFormatBits(AX, AY: Integer; ACount: Integer): Integer;
{* 从指定起始位置沿水平或垂直方向提取 ACount 位格式信息位序列,MSB 优先}
function DecodeFormatInfo(MaskedInfo1, MaskedInfo2: Integer): Boolean;
{* 对两份掩码后的 15 位格式信息进行汉明距离匹配解码}
// ===== 版本信息解码 =====
function ReadVersion: Boolean;
function ExtractVersionBits(AStartCol, AStartRow: Integer): Integer;
{* 从指定位置提取 18 位版本信息位序列,按标准存储布局组合 3 列 x 6 行}
function DecodeVersion(VersionBits1, VersionBits2: Integer): Boolean;
{* 对两份 18 位版本信息进行汉明距离匹配解码}
// ===== 去掩码 =====
function GetMaskPattern(X, Y, MaskType: Integer): Boolean;
{* 获取指定位置在指定掩码类型下的掩码值,与编码器中的逻辑完全一致}
function IsFunctionArea(X, Y: Integer): Boolean;
{* 判断指定位置是否属于功能区域(寻像图案/时序图案/格式信息/版本信息/对齐图案)}
// ===== 码字读取 =====
function ReadCodewords: TBytes;
{* 按 Z 字形路径从矩阵中读取码字字节序列}
// ===== 数据块划分 =====
procedure SplitDataBlocks(const RawCodewords: TBytes; const
AVersion: TCnQRCodeVersion; const AErrorLevel: TCnErrorRecoveryLevel;
var DataBlocks: array of TBytes; var BlockECCPerBlock: Integer;
var TotalDataBytes: Integer);
{* 将交错的码字序列按版本和纠错级别解交织划分为数据块}
// ===== Reed-Solomon 纠错 =====
function RSDecodeBlock(var Data: TBytes; ECCount: Integer): Boolean;
{* 对数据块执行 GF(2^8) Reed-Solomon 纠错(Euclidean 算法)}
function GFMul(A, B: Integer): Integer;
{* GF(2^8) 乘法:LOG[A] + LOG[B] mod 255 后查 EXP 表,0 元素特殊处理}
function GFDiv(A, B: Integer): Integer;
{* GF(2^8) 除法:LOG[A] - LOG[B] + 255 mod 255 后查 EXP 表}
function GFExp(N: Integer): Integer;
{* GF(2^8) 指数:查 CN_EXP_TABLE[N mod 255]}
function GFLog(N: Integer): Integer;
{* GF(2^8) 对数:查 CN_LOG_TABLE[N]}
function GFInv(N: Integer): Integer;
{* GF(2^8) 乘法逆元:CN_EXP_TABLE[255 - CN_LOG_TABLE[N]]}
// ===== 比特流解析 =====
function DecodeDataStream(const DataBytes: TBytes): string;
{* 解析数据字节流,按编码模式段循环解码并返回完整文本}
function ReadBits(var BitPos: Integer; NumBits: Integer;
const DataBytes: TBytes): Integer;
{* 从数据字节流的 BitPos 位置读取 NumBits 位(大端序),返回整数值}
procedure DecodeNumericSegment(var BitPos: Integer; ACount: Integer;
const DataBytes: TBytes; var ResultStr: string);
{* 解码 Numeric 模式段,ACount 为字符数}
procedure DecodeAlphaNumericSegment(var BitPos: Integer; ACount: Integer;
const DataBytes: TBytes; var ResultStr: string);
{* 解码 AlphaNumeric 模式段,ACount 为字符数}
procedure DecodeByteSegment(var BitPos: Integer; ACount: Integer;
const DataBytes: TBytes; var ResultStr: string);
{* 解码 Byte 模式段,ACount 为字节数}
procedure DecodeKanjiSegment(var BitPos: Integer; ACount: Integer;
const DataBytes: TBytes; var ResultStr: string);
{* 解码 Kanji 模式段,ACount 为字符数,按 Shift-JIS 编码}
procedure DecodeHanziSegment(var BitPos: Integer; ACount: Integer;
const DataBytes: TBytes; var ResultStr: string);
{* 解码 Hanzi 模式段,ACount 为字符数,按 GB2312 编码}
// ===== 镜像处理 =====
procedure MirrorMatrix;
{* 将当前矩阵沿主对角线翻转(QRData[X, Y] 与 QRData[Y, X] 互换)}
public
constructor Create;
{* 构造函数,初始化内部变量}
function DecodeMatrix(const AQRData: TCnQRData; ASize: Integer): string;
{* 主解码入口。从 TCnQRData 矩阵解码出文本字符串。
自动尝试镜像重试:正常解码失败后镜像翻转矩阵重新解码。
成功时返回解码文本;失败时抛出 ECnQRCodeException 异常。
异常消息包含具体错误类型和上下文信息。}
property IsMirrored: Boolean read FIsMirrored;
{* 是否通过镜像重试解码成功}
property QRVersion: TCnQRCodeVersion read FQRVersion;
{* 解码得到的二维码版本号}
property FormatInfo: TCnQRFormatInfo read FFormatInfo;
{* 解码得到的格式信息(纠错级别和掩码类型)}
property ErrorMessage: string read FErrorMessage;
{* 最后一次解码失败的错误信息}
end;
{* 二维码解码器类,负责从 TCnQRData 矩阵解码出文本内容}辅助类型
type
TDataBlockArray = array of TBytes;
{* 数据块数组,每个元素为一个数据块的字节序列}常量表补充
格式信息解码查找表
参考 zxing FormatInformation.java 中的 FORMAT_INFO_DECODE_LOOKUP 表。共 32 个条目,每个条目为已异或掩码 0x5412 的 15 位 BCH 码字及其对应的 5 位原始数据(高 2 位为纠错级别,低 3 位为掩码类型):
const
CN_FORMAT_INFO_DECODE_LOOKUP: array[0..31, 0..1] of Integer = (
($5412, $00), ($5125, $01), ($5E7C, $02), ($5B4B, $03),
($45F9, $04), ($40CE, $05), ($4F97, $06), ($4AA0, $07),
($77C4, $08), ($72F3, $09), ($7DAA, $0A), ($789D, $0B),
($662F, $0C), ($6318, $0D), ($6C41, $0E), ($6976, $0F),
($1689, $10), ($13BE, $11), ($1CE7, $12), ($19D0, $13),
($0762, $14), ($0255, $15), ($0D0C, $16), ($083B, $17),
($355F, $18), ($3068, $19), ($3F31, $1A), ($3A06, $1B),
($24B4, $1C), ($2183, $1D), ($2EDA, $1E), ($2BED, $1F));
{* 格式信息解码查找表,用于汉明距离匹配。
第 0 列为已异或掩码的 BCH 码字,第 1 列为原始数据。
原始数据 bits[4..3] = 纠错级别(00=M,01=L,11=Q,10=H),bits[2..0] = 掩码类型}版本信息解码查找表
参考 zxing Version.java 中的 VERSION_DECODE_INFO 数组。共 34 个条目(版本 7..40),每个条目为 18 位 BCH 编码后的版本信息码字:
const
CN_VERSION_DECODE_INFO: array[0..33] of Integer = (
$07C94, $085BC, $09A99, $0A4D3, $0BBF6,
$0C762, $0D847, $0E60D, $0F928, $10B78,
$1145D, $12A17, $13532, $149A6, $15683,
$168C9, $177EC, $18EC4, $191E1, $1AFAB,
$1B08E, $1CC1A, $1D33F, $1ED75, $1F250,
$209D5, $216F0, $228BA, $2379F, $24B0B,
$2542E, $26A64, $27541, $28C69);
{* 版本信息解码查找表,用于汉明距离匹配。
索引 0 对应版本 7,索引 1 对应版本 8,依此类推。
值为 18 位 BCH 编码后的版本信息码字。}数据块结构常量
解码器复用编码器已有的以下常量表,无需新增:
• CN_TOTAL_CODEWORDS:每版本总码字数• CN_NUM_ERROR_CORRECTION_BLOCKS:纠错块数量 [版本, 纠错级别]• CN_ECC_CODEWORDS_PER_BLOCK:每块纠错码字数 [版本, 纠错级别]• CN_ALIGNMENT_PATTERN_COORDINATES_V2..V40:对齐图案坐标• CN_CHAR_COUNT_BITS:字符计数指示符位长 [版本, 模式]• CN_LOG_TABLE/CN_EXP_TABLE:GF(2^8) 对数表和指数表
解码时纠错级别到 CN_NUM_ERROR_CORRECTION_BLOCKS 和 CN_ECC_CODEWORDS_PER_BLOCK 的列索引映射关系与编码器一致(erlL=0, erlM=1, erlQ=2, erlH=3)。
阶段一详细设计:矩阵解码(TCnQRDecoder.DecodeMatrix)
1. 格式信息解码算法
流程:
1. 从两份位置读取 15 位掩码后的格式信息:
第一份:水平方向(行 8,列 0..8 跳过列 6)+ 垂直方向(列 8,行 0..8 跳过行 6)
读取顺序:列 0..5 从行 8 → 列 7 行 8 → 列 8 行 8 → 行 0..5 从列 8 → 行 7 列 8
第二份:右上角(行 8,列 Size-1..Size-8)+ 左下角垂直区域ExtractFormatBits 实现:
水平提取时,按 MSB 优先顺序读取指定起始位置沿 X 正方向的连续 ACount 个模块值。
垂直提取时,按 MSB 优先顺序读取指定起始位置沿 Y 正方向的连续 ACount 个模块值。
DecodeFormatInfo 实现:
function DecodeFormatInfo(MaskedInfo1, MaskedInfo2: Integer): Boolean;
var
I, Dist1, Dist2, BestDist1, BestDist2, BestIdx1, BestIdx2: Integer;
RawVal1, RawVal2: Integer;
begin
// 对两份掩码后的格式信息分别查表
BestDist1 := MaxInt;
BestDist2 := MaxInt;
for I := 0 to 31 do
begin
Dist1 := HammingDistance(MaskedInfo1, CN_FORMAT_INFO_DECODE_LOOKUP[I, 0]);
Dist2 := HammingDistance(MaskedInfo2, CN_FORMAT_INFO_DECODE_LOOKUP[I, 0]);
if Dist1 < BestDist1 then
begin
BestDist1 := Dist1;
BestIdx1 := I;
end;
if Dist2 < BestDist2 then
begin
BestDist2 := Dist2;
BestIdx2 := I;
end;
end;
// 两份中选最小距离的
if BestDist1 <= BestDist2 then
begin
if BestDist1 > 3 then
begin
Result := False;
Exit;
end;
RawVal1 := CN_FORMAT_INFO_DECODE_LOOKUP[BestIdx1, 1];
end
else
begin
if BestDist2 > 3 then
begin
Result := False;
Exit;
end;
RawVal1 := CN_FORMAT_INFO_DECODE_LOOKUP[BestIdx2, 1];
end;
// 解析纠错级别和掩码类型
case (RawVal1 shr 3) and $03 of
1: FFormatInfo.ErrorLevel := erlL;
0: FFormatInfo.ErrorLevel := erlM;
3: FFormatInfo.ErrorLevel := erlQ;
2: FFormatInfo.ErrorLevel := erlH;
end;
FFormatInfo.MaskType := RawVal1 and $07;
Result := True;
end;汉明距离辅助函数:
function CnQRHammingDistance(A, B: Integer; ABitCount: Integer): Integer;
var
XorVal: Integer;
begin
XorVal := A xor B;
Result := 0;
while XorVal <> 0 do
begin
Inc(Result, XorVal and 1);
XorVal := XorVal shr 1;
end;
// 只计算低 ABitCount 位的汉明距离
end;
{* 计算两个整数低 ABitCount 位的汉明距离(异或后 1 的个数)}2. 版本信息解码算法
流程:
1. 版本 ≤ 6:直接由公式 Version = (Size - 17) / 4推算2. 版本 ≥ 7:从右上角和左下角区域读取两份 18 位版本信息 • 右上角:列 Size-11..Size-9,行0..5,按标准布局:for I := 0 to 17 do 位[I] = 矩阵[Size-11+(I mod 3), (I div 3)]• 左下角:列 0..5,行Size-11..Size-9,按标准布局:位[I] = 矩阵[(I div 3), Size-11+(I mod 3)]
DecodeVersion 算法:
function DecodeVersion(VersionBits1, VersionBits2: Integer): Boolean;
var
I, Dist1, Dist2, BestDist1, BestDist2, BestIdx1, BestIdx2: Integer;
begin
BestDist1 := MaxInt;
BestDist2 := MaxInt;
for I := 0 to 33 do
begin
Dist1 := CnQRHammingDistance(VersionBits1, CN_VERSION_DECODE_INFO[I], 18);
Dist2 := CnQRHammingDistance(VersionBits2, CN_VERSION_DECODE_INFO[I], 18);
if Dist1 < BestDist1 then
begin
BestDist1 := Dist1;
BestIdx1 := I;
end;
if Dist2 < BestDist2 then
begin
BestDist2 := Dist2;
BestIdx2 := I;
end;
end;
// 选最小距离的匹配结果
if BestDist1 <= BestDist2 then
begin
if BestDist1 > 3 then
begin
Result := False;
Exit;
end;
FQRVersion := BestIdx1 + 7;
end
else
begin
if BestDist2 > 3 then
begin
Result := False;
Exit;
end;
FQRVersion := BestIdx2 + 7;
end;
// 验证版本号对应的矩阵尺寸
if FQRSize <> FQRVersion * 4 + 17 then
begin
Result := False;
Exit;
end;
Result := True;
end;3. 去掩码算法
复用编码器中的 GetMaskPattern 和 IsFunctionArea 逻辑:
procedure UnmaskMatrix;
var
X, Y: Integer;
MaskType: Integer;
begin
MaskType := FFormatInfo.MaskType;
for X := 0 to FQRSize - 1 do
for Y := 0 to FQRSize - 1 do
if not IsFunctionArea(X, Y) then
if GetMaskPattern(X, Y, MaskType) then
FQRData[X, Y] := 1 - FQRData[X, Y];
end;GetMaskPattern 的 8 种掩码规则(与编码器完全一致):
IsFunctionArea 的解码适配:
解码时的 IsFunctionArea 与编码器逻辑基本一致,区别在于解码时 FQRSize 和 FQRVersion 已预先确定:
• 三个寻像图案:左上 (0..7,0..7)、右上 (Size-8..Size-1,0..7)、左下 (0..7,Size-8..Size-1) • 时序图案:第 6 行和第 6 列 • 格式信息:行 8/列 8 上的特定位置 • 版本信息(版本 ≥ 7):右上角和左下角的 3 列 x 6 行区域 • 对齐图案:根据 FQRVersion 查表确定 5x5 区域,排除与寻像图案重叠的位置
4. Z 字形路径码字读取
参考编码器 PlaceDataBits 的反向逻辑(读取而非写入)。遍历方向与编码器完全一致:
function ReadCodewords: TBytes;
var
BitIndex, Right, Vert, J, X, Y, CodeCount, MaxCodewords: Integer;
Upward: Boolean;
CurrentByte, BitsInCurrentByte: Integer;
begin
MaxCodewords := CN_TOTAL_CODEWORDS[FQRVersion];
SetLength(Result, MaxCodewords);
BitIndex := 0;
BitsInCurrentByte := 0;
CurrentByte := 0;
CodeCount := 0;
Right := FQRSize - 1;
while (Right >= 1) and (CodeCount < MaxCodewords) do
begin
if Right = 6 then
Right := 5; // 跳过时序图案列
Upward := ((Right + 1) and 2) = 0;
for Vert := 0 to FQRSize - 1 do
begin
for J := 0 to 1 do
begin
X := Right - J;
if Upward then
Y := FQRSize - 1 - Vert
else
Y := Vert;
if not IsFunctionArea(X, Y) then
begin
CurrentByte := (CurrentByte shl 1) or FQRData[X, Y];
Inc(BitsInCurrentByte);
if BitsInCurrentByte = 8 then
begin
Result[CodeCount] := CurrentByte;
Inc(CodeCount);
CurrentByte := 0;
BitsInCurrentByte := 0;
end;
end;
end;
end;
Dec(Right, 2);
end;
if CodeCount < MaxCodewords then
SetLength(Result, CodeCount); // 剩余位数不足一个字节时截断
// 验证码字数
if CodeCount <> MaxCodewords then
raise ECnQRCodeException.Create('Codeword count mismatch');
end;5. 数据块划分(解交织)
将已读取的码字序列按 QR 码规范的交织顺序重新分配为各数据块。算法参考 zxing DataBlock.getDataBlocks()。
procedure SplitDataBlocks(const RawCodewords: TBytes;
const AVersion: TCnQRCodeVersion; const AErrorLevel: TCnErrorRecoveryLevel;
var DataBlocks: array of TBytes; var BlockECCPerBlock: Integer;
var TotalDataBytes: Integer);
var
NumBlocks, ShortBlockLen, NumShortBlocks, LongBlockLen, NumLongBlocks: Integer;
TotalCodewords, I, J, K, BlockLen: Integer;
begin
TotalCodewords := CN_TOTAL_CODEWORDS[AVersion];
NumBlocks := CN_NUM_ERROR_CORRECTION_BLOCKS[AVersion, Ord(AErrorLevel)];
BlockECCPerBlock := CN_ECC_CODEWORDS_PER_BLOCK[AVersion, Ord(AErrorLevel)];
// 计算短块和长块的数量与长度
ShortBlockLen := TotalCodewords div NumBlocks;
LongBlockLen := ShortBlockLen + 1;
NumLongBlocks := TotalCodewords mod NumBlocks;
NumShortBlocks := NumBlocks - NumLongBlocks;
TotalDataBytes := NumShortBlocks * (ShortBlockLen - BlockECCPerBlock) +
NumLongBlocks * (LongBlockLen - BlockECCPerBlock);
// 分配数据块
SetLength(DataBlocks, NumBlocks);
K := 0;
// 先分配所有块的数据部分(非交织分配)
for I := 0 to ShortBlockLen - BlockECCPerBlock - 1 do
for J := 0 to NumBlocks - 1 do
begin
if (J < NumShortBlocks) or (I < LongBlockLen - BlockECCPerBlock) then
begin
BlockLen := Length(DataBlocks[J]);
SetLength(DataBlocks[J], BlockLen + 1);
DataBlocks[J][BlockLen] := RawCodewords[K];
Inc(K);
end;
end;
// 再分配所有块的纠错码部分
for I := 0 to BlockECCPerBlock - 1 do
for J := 0 to NumBlocks - 1 do
begin
BlockLen := Length(DataBlocks[J]);
SetLength(DataBlocks[J], BlockLen + 1);
DataBlocks[J][BlockLen] := RawCodewords[K];
Inc(K);
end;
end;6. Reed-Solomon 纠错解码
参考 zxing ReedSolomonDecoder.java 的 Euclidean 算法实现。
GF(2^8) 域运算辅助函数:
function TCFunction.TCQRDecoder.GFMul(A, B: Integer): Integer;
begin
if (A = 0) or (B = 0) then
Result := 0
else
Result := CN_EXP_TABLE[(CN_LOG_TABLE[A] + CN_LOG_TABLE[B]) mod 255];
end;
function TCFunction.TCQRDecoder.GFDiv(A, B: Integer): Integer;
begin
if (A = 0) or (B = 0) then
Result := 0
else
Result := CN_EXP_TABLE[(CN_LOG_TABLE[A] - CN_LOG_TABLE[B] + 255) mod 255];
end;
function TCFunction.TCQRDecoder.GFExp(N: Integer): Integer;
begin
Result := CN_EXP_TABLE[N mod 255];
end;
function TCFunction.TCQRDecoder.GFLog(N: Integer): Integer;
begin
Result := CN_LOG_TABLE[N];
end;
function TCFunction.TCQRDecoder.GFInv(N: Integer): Integer;
begin
Result := CN_EXP_TABLE[255 - CN_LOG_TABLE[N]];
end;RSDecodeBlock 算法(Euclidean 算法):
function RSDecodeBlock(var Data: TBytes; ECCount: Integer): Boolean;
var
TwoS, I, J, K, ErrLocCount: Integer;
Syndrome: array of Integer;
Sigma, Omega: array of Integer;
ErrorLocations: array of Integer;
ErrorEvaluations: array of Integer;
PolyR0, PolyR1, PolyR2: array of Integer;
PolyT0, PolyT1, PolyT2: array of Integer;
DegR0, DegR1, DegR2: Integer;
DegT0, DegT1, DegT2: Integer;
LeadingCoeffQ, TempVal, TempDeg: Integer;
begin
TwoS := ECCount;
// Step 1: 计算伴随式 Syndromes
SetLength(Syndrome, TwoS);
for I := 0 to TwoS - 1 do
begin
Syndrome[I] := 0;
for J := 0 to High(Data) do
Syndrome[I] := Data[J] xor GFMul(Syndrome[I], GFExp(I + 1));
end;
// 检查所有伴随式是否为 0(无错误)
I := 0;
while (I < TwoS) and (Syndrome[I] = 0) do
Inc(I);
if I >= TwoS then
begin
Result := True;
Exit; // 无错误
end;
// Step 2: Euclidean 算法求解错误定位多项式 sigma(x) 和错误估值多项式 omega(x)
// 初始化 r[-1] = x^(2t), r[0] = syndrome(x)
// 初始化 t[-1] = 0, t[0] = 1
SetLength(PolyR0, TwoS + 1);
PolyR0[TwoS] := 1; // x^(2t)
DegR0 := TwoS;
for I := 0 to TwoS - 1 do
PolyR0[I] := 0;
SetLength(PolyR1, TwoS);
for I := 0 to TwoS - 1 do
PolyR1[I] := Syndrome[I];
DegR1 := TwoS - 1;
// 去除高次零系数
while (DegR1 >= 0) and (PolyR1[DegR1] = 0) do
Dec(DegR1);
SetLength(PolyT0, 1);
PolyT0[0] := 0;
DegT0 := 0;
SetLength(PolyT1, 1);
PolyT1[0] := 1;
DegT1 := 0;
// 迭代:r[k-2] = q[k] * r[k-1] + r[k], t[k] = t[k-2] - q[k] * t[k-1]
while (DegR1 >= TwoS div 2) and (DegR1 >= 0) do
begin
// 计算商多项式 q(x) = leading(r[k-2]) / leading(r[k-1]) * x^(deg(r[k-2]) - deg(r[k-1]))
LeadingCoeffQ := GFDiv(PolyR0[DegR0], PolyR1[DegR1]);
TempDeg := DegR0 - DegR1;
// 更新 r[k] = r[k-2] - q * r[k-1]
SetLength(PolyR2, DegR1 + 1);
for I := 0 to DegR1 do
PolyR2[I] := PolyR1[I];
DegR2 := DegR1;
// 实际执行多项式减法(在 GF(2) 上即异或)
for I := 0 to DegR1 do
PolyR0[I + TempDeg] := PolyR0[I + TempDeg] xor GFMul(LeadingCoeffQ, PolyR1[I]);
// 重新确定 deg(r[k])
DegR0 := DegR0;
while (DegR0 >= 0) and (PolyR0[DegR0] = 0) do
Dec(DegR0);
// 更新 t[k] = t[k-2] - q * t[k-1]
SetLength(PolyT2, DegT1 + 1);
for I := 0 to DegT1 do
PolyT2[I] := PolyT1[I];
DegT2 := DegT1;
// 构建 q * t[k-1](首项为 q,次数提升 TempDeg)
for I := 0 to DegT1 do
begin
if PolyT1[I] <> 0 then
begin
K := I + TempDeg;
if K >= Length(PolyT0) then
SetLength(PolyT0, K + 1);
PolyT0[K] := PolyT0[K] xor GFMul(LeadingCoeffQ, PolyT1[I]);
end;
end;
DegT0 := DegT0 + TempDeg;
while (DegT0 >= 0) and (PolyT0[DegT0] = 0) do
Dec(DegT0);
// 交换 r 和 t 的缓冲区
PolyR0 := PolyR2; DegR0 := DegR2;
PolyR1 := PolyR2_copy ?; // 需要暂存
// ... 继续迭代直到 deg(r[k]) < t
end;
// Result: sigma(x) = t[k] / t[k](0), omega(x) = r[k] / t[k](0)
// Step 3: Chien 搜索找错误位置
// 遍历 GF 中非零元 alpha^i,计算 sigma(alpha^i) = 0 得错误位置
// Step 4: Forney 公式计算错误值
// e[k] = omega(xi[k]^(-1)) / sigma'(xi[k]^(-1))
// Step 5: 修复错误
// 将错误值异或到对应位置
Result := True; // 在内部判断错误数量是否超过纠错能力
end;注意:上述 Euclidean 算法的伪代码为简化示意,完整实现需要精确处理多项式除法、余数计算和缓冲区交换。最终实现时参考 zxing
ReedSolomonDecoder.java中的完整逻辑。
7. 比特流解析
模式指示符映射:
字符计数位长度:
解码算法概要:
function DecodeDataStream(const DataBytes: TBytes): string;
var
BitPos: Integer;
ModeVal: Integer;
CharCount: Integer;
Mode: TCnQRDecodeMode;
begin
Result := '';
BitPos := 0;
while BitPos < Length(DataBytes) * 8 do
begin
// 读取 4 位模式指示符
ModeVal := ReadBits(BitPos, 4, DataBytes);
case ModeVal of
0: Break; // TERMINATOR
1: begin // NUMERIC
CharCount := ReadCharCount(BitPos, qrmNumeric, FQRVersion, DataBytes);
DecodeNumericSegment(BitPos, CharCount, DataBytes, Result);
end;
2: begin // ALPHANUMERIC
CharCount := ReadCharCount(BitPos, qrmAlphaNumeric, FQRVersion, DataBytes);
DecodeAlphaNumericSegment(BitPos, CharCount, DataBytes, Result);
end;
4: begin // BYTE
CharCount := ReadCharCount(BitPos, qrmByte, FQRVersion, DataBytes);
DecodeByteSegment(BitPos, CharCount, DataBytes, Result);
end;
7: begin // ECI
// 跳过 ECI 指定值,读取后续 Byte 模式
...
end;
8: begin // KANJI
CharCount := ReadCharCount(BitPos, qrmKanji, FQRVersion, DataBytes);
DecodeKanjiSegment(BitPos, CharCount, DataBytes, Result);
end;
13: begin // HANZI
// 读 4 位子集指示符
...
CharCount := ReadCharCount(BitPos, qrmHanzi, FQRVersion, DataBytes);
DecodeHanziSegment(BitPos, CharCount, DataBytes, Result);
end;
else
raise ECnQRCodeException.Create('Unknown mode indicator');
end;
end;
end;各模式段解码:
• Numeric 模式:每 10 位解码为 0-999 的 3 位数字,7 位解码 2 位数字,4 位解码 1 位数字 • AlphaNumeric 模式:每 11 位解码为 value=char1*45+char2 的两个字符,查表 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:;剩余 1 个字符用 6 位表示• Byte 模式:每 8 位一个字节,逐个追加到结果字符串。字符集优先 UTF-8 自动检测,回退到 ISO-8859-1 • Kanji 模式:每 13 位解码 -> assembledTwoBytes = ((value / 0x0C0) << 8) | (value % 0x0C0);若 assembledTwoBytes < 0x1F00 则 + 0x8140,否则 + 0xC140。得到 Shift-JIS 双字节编码 • Hanzi 模式:每 13 位解码 -> assembledTwoBytes = ((value / 0x060) << 8) | (value % 0x060);若 assembledTwoBytes < 0xA00 则 + 0x0A1A1,否则 + 0x0A6A1。得到 GB2312 双字节编码
8. 镜像矩阵重试
procedure MirrorMatrix;
var
X, Y: Integer;
Temp: Byte;
begin
for X := 0 to FQRSize - 1 do
for Y := X + 1 to FQRSize - 1 do
begin
Temp := FQRData[X, Y];
FQRData[X, Y] := FQRData[Y, X];
FQRData[Y, X] := Temp;
end;
end;DecodeMatrix 主入口的重试逻辑:
function DecodeMatrix(const AQRData: TCnQRData; ASize: Integer): string;
var
Attempt: Integer;
LastError: Exception;
begin
FQRData := AQRData;
FQRSize := ASize;
FIsMirrored := False;
LastError := nil;
for Attempt := 0 to 1 do
begin
try
// Step 1: 读格式信息
if not ReadFormatInformation then
raise ECnQRCodeException.Create('Format information decode failed');
// Step 2: 读版本信息
if not ReadVersion then
raise ECnQRCodeException.Create('Version information decode failed');
// Step 3: 去掩码
UnmaskMatrix;
// Step 4: 读码字
Codewords := ReadCodewords;
// Step 5: 数据块划分
SplitDataBlocks(Codewords, ...);
// Step 6: RS 纠错
for I := 0 to NumBlocks - 1 do
if not RSDecodeBlock(DataBlocks[I], BlockECCPerBlock) then
raise ECnQRCodeException.CreateFmt('Checksum error in block %d', [I]);
// Step 7: 比特流解析
Result := DecodeDataStream(AllDataBytes);
Exit; // 成功
except
on E: Exception do
begin
LastError := E;
if Attempt = 0 then
begin
// 镜像后重试
MirrorMatrix;
FIsMirrored := True;
end
else
raise LastError; // 两次都失败则抛出原始异常
end;
end;
end;
end;阶段二详细设计:图像检测
1. 图像二值化(CnQRBinarize)
函数签名:
function CnQRBinarize(const ABitmap: TBitmap;
out ABinarized: TCnQRData): Boolean;
{* 将 TBitmap 图像二值化输出 TCnQRData 矩阵。
返回 False 表示图像尺寸不足(宽或高 < 21 像素)。}算法步骤:
1. 宽高检查: if (ABitmap.Width < 21) or (ABitmap.Height < 21) then Exit(False)2. 创建等尺寸的二值矩阵 SetLength(ABinarized, Width, Height)3. 统计全局灰度统计以辅助局部阈值 4. 将图像划分为 8x8 子块(水平 BlockCols = Ceil(Width/8),垂直BlockRows = Ceil(Height/8))5. 对每个子块计算局部自适应阈值: • 对子块内每个像素计算灰度值: Gray = (R * 299 + G * 587 + B * 114) div 1000• 统计子块内所有像素的灰度直方图 • 计算灰度最小值 minVal 和最大值 maxVal • 若 maxVal - minVal < 24,使用全局阈值 128• 否则:计算最暗 5% 像素的平均值 avgDark 和最亮 5% 像素的平均值 avgLight,阈值为 (avgDark + avgLight) div 26. 对每个像素: • 确定所属子块 • 若像素灰度值 < 该子块阈值, ABinarized[x, y] := 1(黑色)• 否则 ABinarized[x, y] := 0(白色)7. Result := True
2. 寻像图案定位(CnQRFindFinderPatterns)
函数签名:
function CnQRFindFinderPatterns(const ABinarized: TCnQRData;
AWidth, AHeight: Integer;
out FinderPatterns: array of TCnQRFinderPattern): Boolean;
{* 在二值矩阵中定位三个寻像图案。
FinderPatterns 数组长度至少为 3。
返回 True 表示成功找到三个寻像图案。}关键常量:
• CN_QR_MIN_SKIP = 3:最小行跳跃像素数• CN_QR_MAX_MODULES = 97:版本 40 最大模块数• CN_QR_MAX_FINDER_PATTERN_CANDIDATES = 25:最多候选数量
行扫描算法:
// stateCount[0..4]: 黑白黑白黑的像素连续计数
// state 0: 初始状态,等待遇到黑色
// state 1: 正在计数黑色
// state 2: 正在计数白色
// state 3: 正在计数黑色
// state 4: 正在计数白色 -> 检查 1:1:3:1:1 比例
for Y := 0 to Height - 1 do
begin
// 初始化状态
CurrentState := 0;
stateCount[0..4] := 0;
for X := 0 to Width - 1 do
begin
if ABinarized[X, Y] = 1 then // 黑色
begin
if (CurrentState and 1) = 1 then // 已经在计数黑色
Inc(stateCount[CurrentState])
else if CurrentState = 4 then // 完成一个完整的候选检查
begin
if FoundPatternCross(stateCount) then
HandlePossibleCenter(...);
// 重置:保留最后一个白色段作为下一轮的第一个白色段
stateCount[0] := stateCount[2];
stateCount[1] := stateCount[3];
stateCount[2] := stateCount[4];
stateCount[3] := 1;
stateCount[4] := 0;
CurrentState := 3;
end
else // 切换到黑色计数
begin
Inc(CurrentState);
stateCount[CurrentState] := 1;
end;
end
else // 白色
begin
if (CurrentState and 1) = 0 then // 在计数白色
Inc(stateCount[CurrentState])
else if CurrentState = 0 then // 还没开始
// 忽略前导白色
else // 切换到白色计数
begin
Inc(CurrentState);
stateCount[CurrentState] := 1;
end;
end;
end;
end;比例验证 FoundPatternCross:
function FoundPatternCross(const stateCount: array of Integer): Boolean;
var
TotalModuleSize, I: Integer;
begin
Result := False;
TotalModuleSize := 0;
for I := 0 to 4 do
Inc(TotalModuleSize, stateCount[I]);
if TotalModuleSize < 7 then
Exit;
TotalModuleSize := TotalModuleSize div 7;
// 允许偏差:每段偏差不超过 TotalModuleSize
// 中间段(索引 2)偏差不超过 3 * TotalModuleSize
if (Abs(stateCount[0] - TotalModuleSize) < TotalModuleSize) and
(Abs(stateCount[1] - TotalModuleSize) < TotalModuleSize) and
(Abs(stateCount[2] - 3 * TotalModuleSize) < 3 * TotalModuleSize) and
(Abs(stateCount[3] - TotalModuleSize) < TotalModuleSize) and
(Abs(stateCount[4] - TotalModuleSize) < TotalModuleSize) then
Result := True;
end;交叉验证 CrossCheckVertical:
在候选中心的列坐标处,沿垂直方向扫描黑白黑白黑五段,验证比例是否满足 1:1:3:1:1。垂直方向的 centerFromEnd 公式:
centerFromEnd = startY - stateCount[4] - stateCount[3] - stateCount[2] / 2类似的还有 CrossCheckHorizontal、CrossCheckDiagonal。
四重验证流程:
1. 水平扫描 -> 发现候选 -> 计算 centerFromEnd 列坐标
2. 在列坐标处执行 CrossCheckVertical -> 精确行坐标
3. 以精确行坐标重新 CrossCheckHorizontal -> 进一步精确列坐标
4. 以精确行列坐标执行 CrossCheckDiagonal -> 对角线验证候选点合并:
procedure HandlePossibleCenter(stateCount: array of Integer; CX, CY: Double;
var Candidates: array of TCnQRFinderPattern; var Count: Integer);
var
ModuleSize: Double;
I: Integer;
begin
ModuleSize := TotalPixelCount / 7.0;
// 查找是否与已有候选合并
for I := 0 to Count - 1 do
begin
if Distance(CX, CY, Candidates[I].X, Candidates[I].Y) <
Candidates[I].EstimatedModuleSize * 10 then
begin
// 合并:加权平均
Candidates[I].X := (Candidates[I].X * Candidates[I].ConfirmedCount + CX) /
(Candidates[I].ConfirmedCount + 1);
Candidates[I].Y := (Candidates[I].Y * Candidates[I].ConfirmedCount + CY) /
(Candidates[I].ConfirmedCount + 1);
Candidates[I].EstimatedModuleSize := (Candidates[I].EstimatedModuleSize *
Candidates[I].ConfirmedCount + ModuleSize) / (Candidates[I].ConfirmedCount + 1);
Inc(Candidates[I].ConfirmedCount);
Exit;
end;
end;
// 新增候选点
if Count < Length(Candidates) then
begin
Candidates[Count].X := CX;
Candidates[Count].Y := CY;
Candidates[Count].EstimatedModuleSize := ModuleSize;
Candidates[Count].ConfirmedCount := 1;
Inc(Count);
end;
end;三个最佳图案筛选(selectBestPatterns):
1. 从候选中心中选出确认次数最高的至多 25 个候选 2. 遍历所有三组合,评估每组构成等腰直角三角形的程度: • 计算三边距离平方 a、b、c(a ≤ b ≤ c) • 评分 = |c^2 - 2*b^2| + |c^2 - 2*a^2|• 选择评分最小的组合 3. 验证三个图案的估算模块尺寸相差不超过 40% 4. 对选出的三个图案排序: • 最长边对面的点为左上角 • 剩余两点中,叉积为正的点为右上角,另一点为左下角
3. 对齐图案定位(CnQRFindAlignmentPattern)
函数签名:
function CnQRFindAlignmentPattern(const ABinarized: TCnQRData;
AWidth, AHeight: Integer;
const TopLeft, TopRight, BottomLeft: TCnQRFinderPattern;
out AlignmentPattern: TCnQRAlignmentPattern): Boolean;
{* 在二值矩阵中定位对齐图案。版本 1 或未找到时返回 False。
AlignmentPattern 仅在返回 True 时有效。}算法步骤:
1. 根据三个寻像图案估算版本号: EstimatedVersion = Round((Distance(TopLeft, TopRight) / ModuleSize + 7 - 17) / 4)2. 若版本号 <= 1,返回 False 3. 推算右下角位置: BottomRightX = TopRight.X - TopLeft.X + BottomLeft.X,BottomRightY = TopRight.Y - TopLeft.Y + BottomLeft.Y4. 估算对齐图案中心: • 根据版本号查表 CN_ALIGNMENT_PATTERN_COORDINATES_*获取对齐图案的理论网格坐标• 将理论坐标映射到图像坐标 5. 在估算中心周围搜索满足 1:1:1 比例(黑白黑)的对齐图案: • 初始搜索半径 = EstimatedModuleSize * 4 • 若未找到则半径加倍重试(最多扩大到 16 倍模块尺寸) 6. 对找到的候选执行垂直和水平交叉验证 7. 返回找到的对齐图案或 False
4. 透视变换矩阵(CnQRCalcPerspectiveTransform / CnQRTransformPoint)
函数签名:
procedure CnQRCalcPerspectiveTransform(
const SrcPoints: array of TPointFloat;
const DstPoints: array of TPointFloat;
out Transform: TCnQRPerspectiveTransform);
{* 根据源四边形 4 点和目标四边形 4 点计算 3x3 透视变换矩阵}
function CnQRTransformPoint(const Transform: TCnQRPerspectiveTransform;
X, Y: Single): TPointFloat;
{* 对单点执行透视变换}算法(Wolberg 四边形到四边形映射):
1. 计算 s2q(正方形到四边形)变换矩阵: • 设目标四点为 (x0,y0), (x1,y1), (x2,y2), (x3,y3) • 解线性方程组求 8 个参数:a11,a12,a13,a21,a22,a23,a31,a32 • dx1 = x1 - x2, dy1 = y1 - y2, dx2 = x3 - x2, dy2 = y3 - y2• SumX = x0 - x1 + x2 - x3, SumY = y0 - y1 + y2 - y3• 透视参数:分母归一化等 • 若 SumX = 0 且 SumY = 0(仿射变换退化情况)• 否则解透视参数方程 2. 计算 q2s(四边形到正方形) = s2q 的伴随矩阵(逆变换) 3. 复合变换: quadrilateralToQuadrilateral = s2q(dst) * q2s(src)
变换点坐标:
function CnQRTransformPoint(const Transform: TCnQRPerspectiveTransform;
X, Y: Single): TPointFloat;
var
Denominator: Single;
begin
Denominator := Transform.a31 * X + Transform.a32 * Y + Transform.a33;
Result.X := (Transform.a11 * X + Transform.a12 * Y + Transform.a13) / Denominator;
Result.Y := (Transform.a21 * X + Transform.a22 * Y + Transform.a23) / Denominator;
end;5. 网格采样(CnQRSampleGrid)
函数签名:
function CnQRSampleGrid(const ABinarized: TCnQRData;
AWidth, AHeight: Integer;
const Transform: TCnQRPerspectiveTransform;
ADimension: Integer): TCnQRData;
{* 根据透视变换对二值矩阵进行网格采样,输出 ADimension x ADimension 的规范化矩阵}算法:
1. 创建输出矩阵: SetLength(Result, ADimension, ADimension)2. 对每个网格单元 (col, row),其中col = 0..ADimension-1, row = 0..ADimension-1:• 计算网格中心坐标: SrcX = col + 0.5, SrcY = row + 0.5• 应用 CnQRTransformPoint 逆变换得图像坐标 (imgX, imgY)• 四舍五入取整: PixelX = Round(imgX), PixelY = Round(imgY)• 若像素坐标在图像有效范围内, Result[col, row] = ABinarized[PixelX, PixelY]• 否则 Result[col, row] = 0(白色)3. 返回结果矩阵
维度估算:
function CalcDimension(const TopLeft, TopRight, BottomLeft: TCnQRFinderPattern;
ModuleSize: Double): Integer;
var
Dim1, Dim2: Integer;
begin
// 左上到右上的像素距离除以模块尺寸 + 7
Dim1 := Round(Distance(TopLeft, TopRight) / ModuleSize + 7);
// 左上到左下的像素距离除以模块尺寸 + 7
Dim2 := Round(Distance(TopLeft, BottomLeft) / ModuleSize + 7);
// 取平均值
Result := (Dim1 + Dim2) div 2;
// 调整为满足 (Dimension - 1) mod 4 = 0 的合法值
Result := ((Result - 1) div 4) * 4 + 1;
if Result < 21 then
Result := 21;
end;阶段三详细设计:端到端接口
CnQRDecodeFromMatrix
function CnQRDecodeFromMatrix(const AQRData: TCnQRData;
ASize: Integer): string;
{* 从 TCnQRData 矩阵解码出文本。
成功时返回解码文本;失败时抛出 ECnQRCodeException 异常。
ASize 为矩阵维度。}
var
Decoder: TCnQRDecoder;
begin
if ASize < 21 then
raise ECnQRCodeException.Create('Matrix size too small');
Decoder := TCnQRDecoder.Create;
try
Result := Decoder.DecodeMatrix(AQRData, ASize);
finally
Decoder.Free;
end;
end;CnQRDecodeFromBitmap
function CnQRDecodeFromBitmap(const ABitmap: TBitmap): string;
{* 从 TBitmap 图像解码二维码文本。
成功时返回解码文本;失败时抛出 ECnQRCodeException 异常。
异常消息包含具体失败阶段和原因。}
var
Binarized: TCnQRData;
Width, Height: Integer;
FinderPatterns: array[0..2] of TCnQRFinderPattern;
AlignmentPattern: TCnQRAlignmentPattern;
Transform: TCnQRPerspectiveTransform;
QRData: TCnQRData;
Dimension: Integer;
ModuleSize: Double;
HasAlignment: Boolean;
begin
if (ABitmap = nil) or (ABitmap.Width = 0) or (ABitmap.Height = 0) then
raise ECnQRCodeException.Create('Invalid bitmap');
// Step 1: 二值化
Width := ABitmap.Width;
Height := ABitmap.Height;
if not CnQRBinarize(ABitmap, Binarized) then
raise ECnQRCodeException.Create('Image too small (min 21x21)');
try
// Step 2: 寻像图案定位
if not CnQRFindFinderPatterns(Binarized, Width, Height, FinderPatterns) then
raise ECnQRCodeException.Create('No finder patterns found');
// Step 3: 估算模块尺寸和维度
ModuleSize := CalcModuleSize(FinderPatterns[0], FinderPatterns[1],
FinderPatterns[2]);
Dimension := CalcDimension(FinderPatterns[0], FinderPatterns[1],
FinderPatterns[2], ModuleSize);
// Step 4: 对齐图案定位
HasAlignment := CnQRFindAlignmentPattern(Binarized, Width, Height,
FinderPatterns[0], FinderPatterns[1], FinderPatterns[2],
AlignmentPattern);
// Step 5: 透视变换计算
CnQRCalcPerspectiveTransformWithPatterns(FinderPatterns[0],
FinderPatterns[1], FinderPatterns[2], AlignmentPattern,
HasAlignment, Dimension, Transform);
// Step 6: 网格采样
QRData := CnQRSampleGrid(Binarized, Width, Height, Transform, Dimension);
// Step 7: 矩阵解码
Result := CnQRDecodeFromMatrix(QRData, Dimension);
finally
SetLength(Binarized, 0);
SetLength(QRData, 0);
end;
end;模块尺寸估算函数
function CnQRCalcModuleSize(const TopLeft, TopRight,
BottomLeft: TCnQRFinderPattern): Double;
begin
// 计算左上到右上距离除以 14(两个寻像图案中心间距为 14 个模块)
Result := Distance(TopLeft, TopRight) / 14.0;
// 也计算左上到左下距离
// 取两个方向估算的平均值
Result := (Result + Distance(TopLeft, BottomLeft) / 14.0) / 2.0;
end;错误处理设计
异常类层次
解码器复用已有的 ECnQRCodeException 异常基类,不新增子类以免过度设计。通过异常消息字符串区分错误类型:
镜像重试错误处理
正常解码和镜像重试均失败时,保留并抛出原始异常(第一次解码产生的异常),以保持错误信息的一致性。
单元间依赖
interface uses:
- SysUtils // Exception 类
- Classes // TBytes 类型(Delphi 5 兼容)
- CnBits // TCnBitBuilder(与编码器共享,解码器按需使用)
implementation uses:
- CnWideStrings // WideString 处理(与编码器共享)
- Graphics // TBitmap(阶段二和阶段三需要)编码注意事项
1. 注释格式: {* ... }注释放在被注释实体声明的下一行:TCnQRDecoder = class
...
end;
{* 二维码解码器类 }2. GF 运算:解码器的 GFMul、GFDiv等直接复用编码器的CN_LOG_TABLE和CN_EXP_TABLE,不创建独立的 GF 类。0 元素在所有运算中需作为特例处理。3. TCnQRData 读写性能: TCnQRData是array of array of Byte,访问时注意列优先([X, Y]),与编码器规范一致。4. TBitmap 的像素访问:阶段二中使用 TBitmap.ScanLine实现高效像素批量读取(Delphi 5 兼容方式),或使用Canvas.Pixels[x, y](较慢但简单)。5. 字符集转换:Byte 模式解码使用 UTF-8 自动检测( CnAnsiToUtf8/Utf8ToAnsi等工具函数在CnWideStrings中),回退到 ISO-8859-1。Kanji 和 Hanzi 模式需特定 Shift-JIS 和 GB2312 转换表。
夜雨聆风