乐于分享
好东西不私藏

DeepSeek写的设计文档:给CnQRCode增加二维码解码功能

DeepSeek写的设计文档:给CnQRCode增加二维码解码功能

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. 1. 版本 ≤ 6:直接由公式 Version = (Size - 17) / 4 推算
  2. 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 种掩码规则(与编码器完全一致):

掩码类型
判定条件
0
(Row + Col) mod 2 = 0
1
Row mod 2 = 0
2
Col mod 3 = 0
3
(Row + Col) mod 3 = 0
4
((Row div 2) + (Col div 3)) mod 2 = 0
5
((Row * Col) mod 2) + ((Row * Col) mod 3) = 0
6
(((Row * Col) mod 2) + ((Row * Col) mod 3)) mod 2 = 0
7
(((Row + Col) mod 2) + ((Row * Col) mod 3)) mod 2 = 0

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. 比特流解析

模式指示符映射:

4 位值
TCnQRDecodeMode 枚举值
说明
0000
qrmTerminator
终止符,停止解析
0001
qrmNumeric
数字模式
0010
qrmAlphaNumeric
字母数字模式
0011
qrmStructuredAppend
结构化附加
0100
qrmByte
字节模式
0101
qrmFNC1First
FNC1 第一位置
0111
qrmECI
扩展信道解释
1000
qrmKanji
日文汉字模式
1001
qrmFNC1Second
FNC1 第二位置
1101
qrmHanzi
汉字模式(GB2312)

字符计数位长度:

模式
版本 1-9
版本 10-26
版本 27-40
Numeric
10
12
14
AlphaNumeric
9
11
13
Byte
8
16
16
Kanji
8
10
12
Hanzi
8
10
12

解码算法概要:

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. 1. 宽高检查:if (ABitmap.Width < 21) or (ABitmap.Height < 21) then Exit(False)
  2. 2. 创建等尺寸的二值矩阵 SetLength(ABinarized, Width, Height)
  3. 3. 统计全局灰度统计以辅助局部阈值
  4. 4. 将图像划分为 8x8 子块(水平 BlockCols = Ceil(Width/8),垂直 BlockRows = Ceil(Height/8)
  5. 5. 对每个子块计算局部自适应阈值:
    • • 对子块内每个像素计算灰度值:Gray = (R * 299 + G * 587 + B * 114) div 1000
    • • 统计子块内所有像素的灰度直方图
    • • 计算灰度最小值 minVal 和最大值 maxVal
    • • 若 maxVal - minVal < 24,使用全局阈值 128
    • • 否则:计算最暗 5% 像素的平均值 avgDark 和最亮 5% 像素的平均值 avgLight,阈值为 (avgDark + avgLight) div 2
  6. 6. 对每个像素:
    • • 确定所属子块
    • • 若像素灰度值 < 该子块阈值,ABinarized[x, y] := 1(黑色)
    • • 否则 ABinarized[x, y] := 0(白色)
  7. 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. 1. 从候选中心中选出确认次数最高的至多 25 个候选
  2. 2. 遍历所有三组合,评估每组构成等腰直角三角形的程度:
    • • 计算三边距离平方 a、b、c(a ≤ b ≤ c)
    • • 评分 = |c^2 - 2*b^2| + |c^2 - 2*a^2|
    • • 选择评分最小的组合
  3. 3. 验证三个图案的估算模块尺寸相差不超过 40%
  4. 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. 1. 根据三个寻像图案估算版本号:EstimatedVersion = Round((Distance(TopLeft, TopRight) / ModuleSize + 7 - 17) / 4)
  2. 2. 若版本号 <= 1,返回 False
  3. 3. 推算右下角位置:BottomRightX = TopRight.X - TopLeft.X + BottomLeft.XBottomRightY = TopRight.Y - TopLeft.Y + BottomLeft.Y
  4. 4. 估算对齐图案中心:
    • • 根据版本号查表 CN_ALIGNMENT_PATTERN_COORDINATES_* 获取对齐图案的理论网格坐标
    • • 将理论坐标映射到图像坐标
  5. 5. 在估算中心周围搜索满足 1:1:1 比例(黑白黑)的对齐图案:
    • • 初始搜索半径 = EstimatedModuleSize * 4
    • • 若未找到则半径加倍重试(最多扩大到 16 倍模块尺寸)
  6. 6. 对找到的候选执行垂直和水平交叉验证
  7. 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. 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. 2. 计算 q2s(四边形到正方形) = s2q 的伴随矩阵(逆变换)
  3. 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. 1. 创建输出矩阵:SetLength(Result, ADimension, ADimension)
  2. 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. 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 异常基类,不新增子类以免过度设计。通过异常消息字符串区分错误类型:

错误场景
异常消息前缀
产生阶段
格式信息匹配失败(两 15 位匹配误差 > 3)
'Format information decode failed'
ReadFormatInformation
版本信息匹配失败
'Version information decode failed'
ReadVersion
版本号与矩阵尺寸不匹配
'Version/Size mismatch'
ReadVersion
码字数与预期不符
'Codeword count mismatch'
ReadCodewords
RS 纠错失败
'Checksum error in block N'
RSDecodeBlock
未知模式指示符
'Unknown mode indicator'
DecodeDataStream
字符计数超出范围
'Character count exceeds available bits'
DecodeDataStream
未找到寻像图案
'No finder patterns found'
CnQRFindFinderPatterns
图像尺寸不足
'Image too small (min 21x21)'
CnQRBinarize
无效输入参数
'Invalid bitmap' / 'Matrix size too small'
CnQRDecodeFromXxx

镜像重试错误处理

正常解码和镜像重试均失败时,保留并抛出原始异常(第一次解码产生的异常),以保持错误信息的一致性。

单元间依赖

interface uses:
  - SysUtils       // Exception 类
  - Classes        // TBytes 类型(Delphi 5 兼容)
  - CnBits         // TCnBitBuilder(与编码器共享,解码器按需使用)

implementation uses:
  - CnWideStrings  // WideString 处理(与编码器共享)
  - Graphics       // TBitmap(阶段二和阶段三需要)

编码注意事项

  1. 1. 注释格式{* ... } 注释放在被注释实体声明的下一行
      TCnQRDecoder = class
      ...
      end;
    {* 二维码解码器类 }
  2. 2. GF 运算:解码器的 GFMulGFDiv 等直接复用编码器的 CN_LOG_TABLE 和 CN_EXP_TABLE,不创建独立的 GF 类。0 元素在所有运算中需作为特例处理。
  3. 3. TCnQRData 读写性能TCnQRData 是 array of array of Byte,访问时注意列优先([X, Y]),与编码器规范一致。
  4. 4. TBitmap 的像素访问:阶段二中使用 TBitmap.ScanLine 实现高效像素批量读取(Delphi 5 兼容方式),或使用 Canvas.Pixels[x, y](较慢但简单)。
  5. 5. 字符集转换:Byte 模式解码使用 UTF-8 自动检测(CnAnsiToUtf8/Utf8ToAnsi 等工具函数在 CnWideStrings 中),回退到 ISO-8859-1。Kanji 和 Hanzi 模式需特定 Shift-JIS 和 GB2312 转换表。
基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-05-14 14:18:32 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/624475.html
  2. 运行时间 : 0.095402s [ 吞吐率:10.48req/s ] 内存消耗:4,802.49kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=65817947b0759b660f6daa4b0f3faa33
  1. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_static.php ( 6.05 KB )
  7. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/ralouphie/getallheaders/src/getallheaders.php ( 1.60 KB )
  10. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  11. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  12. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  13. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  14. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  15. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  16. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  17. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  18. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  19. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions_include.php ( 0.16 KB )
  21. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions.php ( 5.54 KB )
  22. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  23. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  24. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  25. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/provider.php ( 0.19 KB )
  26. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  27. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  28. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  29. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/common.php ( 0.03 KB )
  30. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  32. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/alipay.php ( 3.59 KB )
  33. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  34. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/app.php ( 0.95 KB )
  35. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cache.php ( 0.78 KB )
  36. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/console.php ( 0.23 KB )
  37. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cookie.php ( 0.56 KB )
  38. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/database.php ( 2.48 KB )
  39. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/filesystem.php ( 0.61 KB )
  40. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/lang.php ( 0.91 KB )
  41. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/log.php ( 1.35 KB )
  42. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/middleware.php ( 0.19 KB )
  43. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/route.php ( 1.89 KB )
  44. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/session.php ( 0.57 KB )
  45. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/trace.php ( 0.34 KB )
  46. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/view.php ( 0.82 KB )
  47. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/event.php ( 0.25 KB )
  48. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  49. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/service.php ( 0.13 KB )
  50. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/AppService.php ( 0.26 KB )
  51. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  52. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  53. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  54. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  55. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  56. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/services.php ( 0.14 KB )
  57. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  58. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  59. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  60. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  61. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  62. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  63. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  64. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  65. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  66. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  67. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  68. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  69. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  70. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  71. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  72. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  73. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  74. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  75. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  76. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  77. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  78. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  79. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  80. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  81. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  82. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  83. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  84. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  85. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  86. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  87. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/Request.php ( 0.09 KB )
  88. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  89. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/middleware.php ( 0.25 KB )
  90. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  91. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  92. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  93. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  94. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  95. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  96. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  97. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  98. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  99. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  100. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  101. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  102. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  103. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/route/app.php ( 3.94 KB )
  104. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  105. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  106. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Index.php ( 9.87 KB )
  108. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/BaseController.php ( 2.05 KB )
  109. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  110. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  111. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  112. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  113. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  114. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  115. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  116. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  117. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  118. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  119. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  120. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  121. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  122. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  123. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  124. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  125. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  126. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  127. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  128. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  129. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  130. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  131. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  132. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  133. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  134. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  135. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Es.php ( 3.30 KB )
  136. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  137. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  138. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  139. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  140. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  141. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  142. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  143. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  144. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/runtime/temp/c935550e3e8a3a4c27dd94e439343fdf.php ( 31.50 KB )
  145. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000528s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000685s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000316s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000289s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000470s ]
  6. SELECT * FROM `set` [ RunTime:0.000192s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000570s ]
  8. SELECT * FROM `article` WHERE `id` = 624475 LIMIT 1 [ RunTime:0.000530s ]
  9. UPDATE `article` SET `lasttime` = 1778739512 WHERE `id` = 624475 [ RunTime:0.003708s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000635s ]
  11. SELECT * FROM `article` WHERE `id` < 624475 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000425s ]
  12. SELECT * FROM `article` WHERE `id` > 624475 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000389s ]
  13. SELECT * FROM `article` WHERE `id` < 624475 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000846s ]
  14. SELECT * FROM `article` WHERE `id` < 624475 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.000947s ]
  15. SELECT * FROM `article` WHERE `id` < 624475 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.008050s ]
0.097093s