likes
comments
collection
share

逐行逐句的教你实现二维码的生成

作者站长头像
站长
· 阅读数 36

前言

二维码的生成大家在日常开发中应该会经常遇到,今天带领大家学习二维码是如何生成。

❗️本文中出现的代码均是由 qrcode-generator 整理归纳并简化而来

起步

想要生成二维码我们需要得到数据格式数据体级别纠错级别蒙版类型

数据格式共有4种,不同的数据格式意味着数据的存储与识别需按照对应的规范,本文将指定为的8bit存储方式。

逐行逐句的教你实现二维码的生成 创建函数如下:

/**
 * 生成二维码
 * @param {Number} text 文本
 * @param {Number} typeNumber 级别
 * @param {Number} errorCorrectionLevel 纠错级别
 * @param {Number} typeNumber 级别
 */
function create(text, typeNumber, errorCorrectionLevel, maskPattern) {

}

1、二维码级别

二维码的级别为[1-40]的正整数,值越大二维码尺寸越大,存储数据的空间越大,同样的,生成与识别的复杂度也越大。

二维码的尺寸与 Version 存在以下线性关系:Size = 21 + (Version - 1) * 4

即为Version为2的二维码拥有25 * 25个单元格,Version为3的二维码拥有29 * 29个单元格

function create(text, typeNumber, errorCorrectionLevel, maskPattern) {
    var _moduleCount = typeNumber * 4 + 17;
    // 1、二维码规格
    _modules = moduleSize(_moduleCount)
}


/**
 * 1、初始化二维码尺寸 
 * @param {Number} moduleCount
 * @returns {Array}
 */
function moduleSize(moduleCount) {
    var modules = new Array(moduleCount);
    for (var row = 0; row < moduleCount; row += 1) {
        modules[row] = new Array(moduleCount);
        for (var col = 0; col < moduleCount; col += 1) {
            modules[row][col] = null;
        }
    }
    return modules;
}

2、定位图案

一个 7×7 的矩阵,用于标记二维码矩形的大小;用三个定位图案即可标识并确定一个二维码矩形的倾斜角度和旋转度数;

逐行逐句的教你实现二维码的生成

定位图案的大小与位置均为固定 与任何变量无关;

function create(text, typeNumber, errorCorrectionLevel, maskPattern) {
    ...
    // 2、定位图案
    setupPositionProbePattern(0, 0);
    setupPositionProbePattern(_moduleCount - 7, 0);
    setupPositionProbePattern(0, _moduleCount - 7);
}
/**
 * 2、绘制定位图案 
 * @param {Number} row 列
 * @param {Number} col 行
 */
function setupPositionProbePattern(row, col) {
    // 以row、col为坐标原点;r为横坐标,c为负向纵坐标
    for (var r = -1; r <= 7; r += 1) {
        if (row + r <= -1 || _moduleCount <= row + r) continue;
        for (var c = -1; c <= 7; c += 1) {
            if (col + c <= -1 || _moduleCount <= col + c) continue;
            // 横向 竖向 中心
            if ((0 <= r && r <= 6 && (c == 0 || c == 6)) || (0 <= c && c <= 6 && (r == 0 || r == 6)) || (2 <= r && r <= 4 && 2 <= c && c <= 4)) {
                _modules[row + r][col + c] = true;
            } else {
                _modules[row + r][col + c] = false;
            }
        }
    }
};

3、对齐图案

一个 5×5 的矩阵,用于标记辅助识别;

逐行逐句的教你实现二维码的生成

version = 2 时对齐图案位置如图

逐行逐句的教你实现二维码的生成

version = 7 时对齐图案位置如图

逐行逐句的教你实现二维码的生成

对齐图案数量与位置仅与二维码级别有关;数据量过大,不在此展示,可见代码仓库

function create(text, typeNumber, errorCorrectionLevel, maskPattern) {
    ...
    // 3、对齐图案
    setupPositionAdjustPattern(typeNumber);
}
/**
 * 3、对齐图案 
 * @param {Number} typeNumber 级别
 */
function setupPositionAdjustPattern(typeNumber) {
    // 对齐图案可放置坐标 注:值为中心点
    var pos = PATTERN_POSITION_TABLE[typeNumber - 1];
    for (var i = 0; i < pos.length; i += 1) {
        for (var j = 0; j < pos.length; j += 1) {
            var row = pos[i];
            var col = pos[j];
            if (_modules[row][col] != null) {
                continue;
            }
            for (var r = -2; r <= 2; r += 1) {
                for (var c = -2; c <= 2; c += 1) {
                    if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) {
                        _modules[row + r][col + c] = true;
                    } else {
                        _modules[row + r][col + c] = false;
                    }
                }
            }
        }
    }
};

4、时序图案

表现为两条连接定位图案的斑马线,出现位置固定在第8行与第8列;

逐行逐句的教你实现二维码的生成

function create(text, typeNumber, errorCorrectionLevel, maskPattern) {
    ...
    // 4、时序图案
    setupTimingPattern();
}
/**
 * 4、时序图案 
 */
function setupTimingPattern() {
    // 纵向
    for (var r = 8; r < _moduleCount - 8; r += 1) {
        if (_modules[r][6] != null) {
            continue;
        }
        _modules[r][6] = (r % 2 == 0);
    }
    // 横向
    for (var c = 8; c < _moduleCount - 8; c += 1) {
        if (_modules[6][c] != null) {
            continue;
        }
        _modules[6][c] = (c % 2 == 0);
    }
};

5、格式信息

横纵两条,包含纠错级别蒙版信息

逐行逐句的教你实现二维码的生成

这里仅存储信息,并不涉及纠错与蒙版实际逻辑

function create(text, typeNumber, errorCorrectionLevel, maskPattern) {
    ...
    var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
  
    // 5、格式信息
    setupTypeInfo(_errorCorrectionLevel, maskPattern);
}


// 纠错码级别
var QRErrorCorrectionLevel = {
    L: 1,
    M: 0,
    Q: 3,
    H: 2
};

/**
 * 5、格式信息
 * @param {Number} _errorCorrectionLevel 纠错级别
 * @param {Number} maskPattern 蒙版索引
*/
function setupTypeInfo(_errorCorrectionLevel, maskPattern) {
    // 纠错级别左移动3位 与 蒙版 或运算 [x,x,y,y,y]
    var data = (_errorCorrectionLevel << 3) | maskPattern;
    var bits = QRUtil.getBCHTypeInfo(data);

    // 纵向
    for (var i = 0; i < 15; i += 1) {
        var mod = ((bits >> i) & 1) == 1;
        if (i < 6) {
            _modules[i][8] = mod;
        } else if (i < 8) {
            _modules[i + 1][8] = mod;
        } else {
            _modules[_moduleCount - 15 + i][8] = mod;
        }
    }

    // 横向
    for (var i = 0; i < 15; i += 1) {
        var mod = ((bits >> i) & 1) == 1;
        if (i < 8) {
            _modules[8][_moduleCount - i - 1] = mod;
        } else if (i < 9) {
            _modules[8][15 - i - 1 + 1] = mod;
        } else {
            _modules[8][15 - i - 1] = mod;
        }
    }
    _modules[_moduleCount - 8][8] = true;
};

const QRUtil = {
    // 5、格式信息
    getBCHTypeInfo: function (data) {
        // 左移10位
        var d = data << 10;
        // 若G15二进制位数(11) 小于d
        while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(G15) >= 0) {
            // 将G15左移补齐 与 d 异或运算
            d ^= (G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(G15)));
        }
        // data左移10位与 d 或运算的结果 与 G15_MASK 异或
        return ((data << 10) | d) ^ G15_MASK;
    },
    getBCHDigit: function (data) {
        var digit = 0;
        while (data != 0) {
            digit += 1;
            data >>>= 1;
        }
        return digit;
    },
}

6、版本信息

用于 Version 7 以上,两块 3×6 的区域存放版本信息

逐行逐句的教你实现二维码的生成

function create(text, typeNumber, errorCorrectionLevel, maskPattern) {
    ...
    // 6、版本信息
    if (typeNumber >= 7) {
        setupTypeNumber(typeNumber);
    }
}

/**
 * 6、版本信息 
 * @param {Number} typeNumber 级别
 */
function setupTypeNumber(typeNumber) {
    var bits = QRUtil.getBCHTypeNumber(typeNumber);
    for (var i = 0; i < 18; i += 1) {
        var mod = ((bits >> i) & 1) == 1;
        _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod;
    }

    for (var i = 0; i < 18; i += 1) {
        var mod = ((bits >> i) & 1) == 1;
        _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
    }
};

const QRUtil = {
    ...
    // 6、版本信息
    getBCHTypeNumber: function (data) {
        var d = data << 12;
        while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(G18) >= 0) {
            d ^= (G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(G18)));
        }
        return (data << 12) | d;
    },
}

7、数据处理

数据码

二维码的数据容量由级别纠错级别数据格式三部分决定

逐行逐句的教你实现二维码的生成

逐行逐句的教你实现二维码的生成

逐行逐句的教你实现二维码的生成

function create(text, typeNumber, errorCorrectionLevel, maskPattern) {
    // 7、格式化数据
    _dataCache = createData(typeNumber, _errorCorrectionLevel);
}

/**
 * 7、格式化数据 
 * @param {Number} typeNumber 级别
 * @param {Number} _errorCorrectionLevel 纠错级别
 */
function createData(typeNumber, errorCorrectionLevel) {
    var rsBlocks = QRUtil.getRSBlocks(typeNumber, errorCorrectionLevel);
    var buffer = QRUtil.qrBitBuffer();
    // UTF-8 转 二进制数组
    var data = QRUtil.qr8BitByte(text);
    // 填充 0100 占4位
    buffer.put(data.getMode(), 4);
    // 填充 数据长度 占位 8位 或 16位
    buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber));
    // 填充数据
    data.write(buffer);

    // calc num max data.
    var totalDataCount = 0;
    for (var i = 0; i < rsBlocks.length; i += 1) {
        totalDataCount += rsBlocks[i].dataCount;
    }

    if (buffer.getLengthInBits() > totalDataCount * 8) {
        throw 'code length overflow. ('
        + buffer.getLengthInBits()
        + '>'
        + totalDataCount * 8
        + ')';
    }

    // end code
    if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
        buffer.put(0, 4);
    }

    // padding
    while (buffer.getLengthInBits() % 8 != 0) {
        buffer.putBit(false);
    }

    // padding
    while (true) {
        if (buffer.getLengthInBits() >= totalDataCount * 8) {
            break;
        }
        buffer.put(PAD0, 8);
        if (buffer.getLengthInBits() >= totalDataCount * 8) {
            break;
        }
        buffer.put(PAD1, 8);
    }
    return QRUtil.createBytes(buffer, rsBlocks);
};

纠错码

关于纠错码的实际原理,碍于篇幅长度这里不再复述,可以看我另外一篇文章:里德 - 所罗门纠错码的原理与实现

蒙版

大片连续的区域粘连在一起会非常不利于二维码的识别与读取,为了避免这种情况,需要使用蒙版对数据区域进行过滤。将数据以指定的规则进行取反操作,使得黑色块分布的更加松散与均匀 一共有8种蒙版:

逐行逐句的教你实现二维码的生成

逐行逐句的教你实现二维码的生成

实际代码如下:

function create(text, typeNumber, errorCorrectionLevel, maskPattern) {
    // 8、蒙版
    maskData(_dataCache, maskPattern);
}

/**
 * 8、蒙版 
 * @param {Number} typeNumber 级别
 */
function maskData(data, maskPattern) {
    var inc = -1;
    var row = _moduleCount - 1;
    var bitIndex = 7;
    var byteIndex = 0;
    var maskFunc = QRUtil.getMaskFunction(maskPattern);

    for (var col = _moduleCount - 1; col > 0; col -= 2) {
        if (col == 6) col -= 1;
        while (true) {
            for (var c = 0; c < 2; c += 1) {
                if (_modules[row][col - c] == null) {
                    var dark = false;
                    if (byteIndex < data.length) {
                        dark = (((data[byteIndex] >>> bitIndex) & 1) == 1);
                    }
                    var mask = maskFunc(row, col - c);
                    // 蒙层
                    if (mask) {
                        dark = !dark;
                    }
                    _modules[row][col - c] = dark;
                    bitIndex -= 1;
                    if (bitIndex == -1) {
                        byteIndex += 1;
                        bitIndex = 7;
                    }
                }
            }
            row += inc;
            if (row < 0 || _moduleCount <= row) {
                row -= inc;
                inc = -inc;
                break;
            }
        }
    }
};

// 蒙版级别
var QRMaskPattern = {
  PATTERN000: 0,
  PATTERN001: 1,
  PATTERN010: 2,
  PATTERN011: 3,
  PATTERN100: 4,
  PATTERN101: 5,
  PATTERN110: 6,
  PATTERN111: 7
};

function getMaskFunction(maskPattern) {
    switch (maskPattern) {
      case QRMaskPattern.PATTERN000:
        return function (i, j) { return (i + j) % 2 == 0; };
      case QRMaskPattern.PATTERN001:
        return function (i, j) { return i % 2 == 0; };
      case QRMaskPattern.PATTERN010:
        return function (i, j) { return j % 3 == 0; };
      case QRMaskPattern.PATTERN011:
        return function (i, j) { return (i + j) % 3 == 0; };
      case QRMaskPattern.PATTERN100:
        return function (i, j) { return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0; };
      case QRMaskPattern.PATTERN101:
        return function (i, j) { return (i * j) % 2 + (i * j) % 3 == 0; };
      case QRMaskPattern.PATTERN110:
        return function (i, j) { return ((i * j) % 2 + (i * j) % 3) % 2 == 0; };
      case QRMaskPattern.PATTERN111:
        return function (i, j) { return ((i * j) % 3 + (i + j) % 2) % 2 == 0; };
      default:
        throw 'bad maskPattern:' + maskPattern;
    }
  }

我这里指定maskPattern为其中一个,实际上在qrcode-generator中则是重复绘制8次选择其中分布最为均匀的模板。

8、绘制

关于绘制,qrcode-generator中选择输出base64的格式,我这里为了图省事,直接使用canvas对数据进行了绘制,实际代码如下:

<script>
    var update_qrcode = function () {
        var form = document.forms['qrForm'];
        var text = form.elements['msg'].value;
        var TypeNumber = 2;
        var ErrorCorrectionLevel = 'M';
        // var Mode = 'Byte';
        // var Multibyte = 'UTF-8';

        create('myCanvas', text, TypeNumber, ErrorCorrectionLevel);
    };
</script>
function create(el, text, typeNumber, errorCorrectionLevel) {
    // 9、绘制
    draw(el, _modules)
}

/**
 * 9、绘制 
 * @param {String} el 元素ID
 * @param {Array} data 数据
 */
function draw(el, data) {
    const cellSize = 3;
    const margin = 4;
    const canvas = document.getElementById(el)
    canvas.style.border = '1px solid #000';
    canvas.width = cellSize * data.length + margin * 2
    canvas.height = cellSize * data.length + margin * 2
    const qr = canvas.getContext("2d");
    qr.fillStyle = "#000";
    for (let row = 0; row < data.length; row++) {
        for (let col = 0; col < data[row].length; col++) {
            const i = data[row][col];
            if (i) {
                qr.fillRect(row * cellSize + margin, col * cellSize + margin, cellSize, cellSize)
            }
        }
    }
}

篇幅表述内容有限,有兴趣的小伙伴可以查看完整代码,仓库如下:gitee.com/ws-code-sto…

转载自:https://juejin.cn/post/7139729196674187301
评论
请登录