二维码生成原理探索

1.前言

demo链接 

 

涉及js库和技术

  1. qrcode-generator 链接
  2. 画布canvas  

 

1.1 什么是二维码?

二维码 (2-dimensional bar code),是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的。

QR码的信息量和版本 (对应参数typeNumber)

QR码设有1到40的不同版本(种类),每个版本都具备固有的码元结构(码元数)。(码元是指构成QR码的方形黑白点。)

“码元结构”是指二维码中的码元数。从版本1(21码元×21码元)开始,在纵向和横向各自以4码元为单位递增,一直到版本40(177码元×177码元)。

QR码的纠错 (对应参数ErrorCorrectionLevel)

QR码具有“纠错功能”。即使编码变脏或破损,也可自动恢复数据。这一“纠错能力”具备4个级别,用户可根据使用环境选择相应的级别。调高级别,纠错能力也相应提高,但由于数据量会随之增加,编码尺寸也也会变大。
用户应综合考虑使用环境、编码尺寸等因素后选择相应的级别。 在工厂等容易沾染赃物的环境下,可以选择级别Q或H,在不那么脏的环境下,且数据量较多的时候,也可以选择级别L。一般情况下用户大多选择级别M(15%)


1.2 常用的生成二维码的库

qrcodejs 链接

qrcode demo 

对于二维码的生成,大家基本都是使用qrcdoejs。

使用 new QRCode(element, option)

参数说明

名称默认值说明
element-显示二维码的元素或该元素的 ID
option 参数
 

option参数说明

名称默认值说明
width256图像宽度
height256图像高度
typeNumber4 
colorDark"#000000"前景色
colorLight"#ffffff"背景色
correctLevelQRCode.CorrectLevel.L容错级别,可设置为:

QRCode.CorrectLevel.L

QRCode.CorrectLevel.M

QRCode.CorrectLevel.Q

QRCode.CorrectLevel.H

 

2.技术分析

2.1 原理分析

二维码 (2-dimensional bar code),是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的。

把一张二维码放在直角坐标系中, 黑白块看成一个一个的点坐标, 按照一定的规则绘制黑点 或者白点。(这里需要解决两个问题 1.横纵 分别有多少个点, 2.什么时候画黑点,什么时候画白点

qrcode-generator 库提供的函数, 解决上述两个问题

剩下的就是在画布上绘制响应的黑白点了。

 
 // qrcode-generator 提供的 函数 
    // link https://kazuhikoarase.github.io/qrcode-generator/js/demo/
    /**
     *
     * @param typeNumber 二维码的码元 即二维码横向有多少个小点
     * @param errorCorrectionLevel 二维码的容错 L M Q H
     * @param data 二维码的信息
     * @returns CacheData
     */
    getQRCodeData(typeNumber: TypeNumber | undefined, errorCorrectionLevel: ErrorCorrectionLevel | undefined, data:string):CacheData{
            const qr = qrcode(typeNumber || 1, errorCorrectionLevel || 'M');
            qr.addData(data || '无数据');
            qr.make();


            // 生成 二维码 横纵有多少个点
            const count = qr.getModuleCount();
            // isDark 接收x, y两个参数 用于判断 该点是否 是否是黑点
            const isDark = qr.isDark;
            this.cacheData = {
                count,
                isDark
            }
            return this.cacheData;
    }




//使用 画布提供的api
//fillStyle
//fillRect 绘制响应颜色的矩形
context.fillStyle = xxx;
context.fillRect( bWidth + n * cellSize, bWidth + j * cellSize , cellSize, cellSize );

 

2.2 具体代码实现

通过rander  调用相应函数

getQRCodeData

calcData

drawQRCode

drawImg

drawText

函数进行 绘制二维码生成logo 边框

render(canvas: HTMLCanvasElement | undefined , config: RenderConfig = {}){
    let { typeNumber,errorCorrectionLevel, logo, data } = this.options;
    let { size, cellSize } = config;


    // 计算获取 1.二维码的横纵点数 2. 获取 isDark 函数
    this.getQRCodeData(typeNumber , errorCorrectionLevel, data as string);


    // 计算 二维码大小 即canvas 大小
    canvas = this.calcData(canvas,size as number);


    // 画 二维码
    this.drawQRCode(size as number,cellSize as number);


    // 绘制logo
    if( logo ){
        if( (logo as LogoOptions).type == 1 ){
            this.drawImg(size as number,false)
        }
        if( (logo as LogoOptions).type == 0){
            this.drawText(size as number)
        }
    }
    return canvas;
}

2.2.1 第一步  getQRCodeData   计算获取 1.二维码的横纵点数 2. 获取 isDark 函数

// qrcode-generator 提供的 函数 
// link https://kazuhikoarase.github.io/qrcode-generator/js/demo/
/**
 *
 * @param typeNumber 二维码的码元 即二维码横向有多少个小点
 * @param errorCorrectionLevel 二维码的容错 L M Q H
 * @param data 二维码的信息
 * @returns CacheData
 */
getQRCodeData(typeNumber: TypeNumber | undefined, errorCorrectionLevel: ErrorCorrectionLevel | undefined, data:string):CacheData{
        const qr = qrcode(typeNumber || 1, errorCorrectionLevel || 'M');
        qr.addData(data || '无数据');
        qr.make();
        // 生成 二维码 横纵有多少个点
        const count = qr.getModuleCount();
        // isDark 接收x, y两个参数 用于判断 该点是否 是否是黑点
        const isDark = qr.isDark;
        this.cacheData = {
            count,
            isDark
        }
        return this.cacheData;
}

2.2.1 第二步  calcData 计算 二维码大小 即canvas 大小

calcData(canvas: HTMLCanvasElement | undefined  ,size: number){
    let { border } = this.options;
    const flag = canvas === undefined;
    if(flag){
        let s:number;
        if(border?.width ){
            s = size + 2 * (border.width || 0);
        }else{
            s = size;
        }
        canvas = getDefaultCanvas(s)
        console.warn('没有给定canvas,由qrcodecanvas 生成canvas')
    }else {
        const width = ( border?.width || 0 ) * 2 + size;
        canvas = canvas as HTMLCanvasElement;
        canvas.width = width;
        canvas.height = width;
    }
    this.canvas = canvas;
     
    return canvas;
}

2.2.1 第三步  drawQRCode 绘制二维码

drawQRCode(size:number,cellSize:number){
    let { bgColor, foreColor, outColor, inColor, border } = this.options;
    const canvas = this.canvas as HTMLCanvasElement;
    const context:any = canvas.getContext("2d");
    const { count, isDark } = this.cacheData as CacheData;
    border = border as Border;
    const bWidth  = border.width || 0;


    // 二维码的定位, 即二维码 左右上角以及左下角的 大方框 这里是固定的
    const foreground = [
        // 判断外边框
        { row: 0, rows: 7, col: 0, cols: 7, style: outColor || foreColor ||  COLOR_BLACK },
        { row: count-7, rows: count, col: 0, cols: 7, style: outColor || foreColor ||  COLOR_BLACK},
        { row: 0, rows: 7, col: count-7, cols: count, style: outColor || foreColor ||  COLOR_BLACK},
        // 内框
        { row: 2, rows: 5, col: 2, cols: 5, style: inColor|| foreColor ||  COLOR_BLACK },
        { row: count-5, rows: count-2, col: 2, cols: 5, style: inColor|| foreColor ||  COLOR_BLACK },
        { row: 2, rows: 5, col: count-5, cols: count-2, style: inColor || foreColor || COLOR_BLACK},
    ];


    if(!cellSize){
        cellSize = size / count;
    }else{
        cellSize = Math.floor(size / count) > cellSize ? Math.floor(size / count) : cellSize
    }


     
    //绘制边框
    context.fillStyle = border.color ||  COLOR_WHITE;
    context.fillRect( 0 , 0, size + 2 * bWidth, size + 2 * bWidth );


    //绘制背景
    context.fillStyle = bgColor || COLOR_WHITE;
    context.fillRect( bWidth, bWidth, size, size );


    //绘制二维码
    for( var n=0 ; n < count; n++ ){
        for( var j = 0; j < count; j++ ){
            if( isDark(n,j) ){
                context.fillStyle = foreColor;
                //todo 是否提出循环
                foreground.forEach(function(el){
                    if((n>= el.col&& n<el.cols) && (j>=el.row && j<el.rows)){
                        context.fillStyle = el.style;
                    }
                })
                // 绘制小点 用矩形
                context.fillRect( bWidth + n * cellSize, bWidth + j * cellSize , cellSize, cellSize );
            }
        }
    }
}

2.2.1 第四步  drawText 绘制logo 文字和背景

drawText(size:number){
    const logo = this.options.logo as LogoText;
    const border  = this.options.border as Border;
    const bWidth = border.width || 0;


    let fontSize:number;


    // 通过 正则获取font中的 fontsize 没有默认 16
    if(logo.font){
        const regexp = /[0-9]*/g;
        let res: any = logo.font.match(regexp);
        res = res ? res[0] : 16;
        fontSize = Number(res) ;
    }else{
        fontSize = 16;
    }
     
    //  先获取双字节文字长度  处理之后 * fontsiz 得到 文字背景宽度
    const w = getStringLength(logo.data) / 2 * fontSize;


    // 文字绘制的 x轴坐标
    const x = (size + bWidth -  w )/ 2;
     
    // 文字绘制的 x轴坐标
    const y = (size + bWidth - fontSize) / 2;
    const context:any = (this.canvas as HTMLCanvasElement).getContext("2d");


    // 绘制文字背景
    context.fillStyle = logo.bgColor || COLOR_WHITE;
    context.fillRect(x,y,w,fontSize);


    //设置文字 font color 等样式
    context.fillStyle = logo.color || COLOR_BLACK;
    context.font = logo.font;
    context.textAlign = 'center';
    context.textBaseline = "middle";
     
    // 绘制文字
    context.fillText( logo.data, x + w / 2, y + fontSize /2 );
}

2.2.1 第5步  drawImg 绘制 logo图标

drawImg(size:number,isBg:boolean){
    const logo = this.options.logo as LogoImg;


    let scaleSize = 0;
    // 计算 图片在 二维码中的大小 ,防止 图片过大 导致二维码无法扫描
    if(logo.size){
        scaleSize = Math.floor(size / 4)  > logo.size ? logo.size : Math.floor(size / 4);
    }else{
        scaleSize = Math.floor(size / 4);
    }




    const border  = this.options.border as Border;
    const bWidth = border.width || 0 ;


    // 得到图片绘制的 位置, 只是简单处理认为 图片是正方形
    const loc = (bWidth + size - scaleSize) / 2;
     
    const context:any =  (this.canvas as HTMLCanvasElement).getContext("2d");
    loadImage(logo.data).then(function(res){
        const img:HTMLImageElement = new Image();
        img.src = res as string;
        img.onload = function(){
            // 绘制二维码
            context.drawImage( this, loc , loc, scaleSize, scaleSize )
        }


    }).catch(function(err){
        throw Error('The server where the picture resides cannot be cross-domain'+err);
    });
}

注:如果canvas上画了图片 图片使用的跨域图片,使用 canvas.toDataURL 导出图片的时候回报跨域错误
通过 ajax 请求 返回 图片blob流 

function loadImage(src:string):Promise<any>{
    return new Promise((resolve, reject) => {
        let xhr:any = null;
        if (window.XMLHttpRequest)
            xhr = new XMLHttpRequest();
        else if (window.ActiveXObject)
            xhr = new ActiveXObject('Microsoft.XMLHTTP');
         
        xhr.onload = () => {
            if (xhr.status === 200) {
                const reader = new FileReader();
                reader.addEventListener('load', () => resolve(reader.result as string), false);
                reader.addEventListener('error', e => reject(e), false);
                reader.readAsDataURL(xhr.response);
                 
            } else {
                reject(`Failed to proxy resource ${src} with status code ${xhr.status}`);
            }
        };




        xhr.onerror = reject;
        xhr.open('GET', src,true);
        // if(window?.loc?.origin){
        //     xhr.setRequestHeader('origin', window.loc.origin);
        // }
         
        xhr.responseType = 'blob';
        //xhr.timeout = 1000 * 90;
        //xhr.ontimeout = () => reject(`Timed out (${timeout}ms) proxying ${src}`);


        xhr.send();
    });
}

4.其他思考

1.其他的定制方面

2.如何接入vue 形成 vue 组件

5.其他链接

可选参数

# options

- typeNumber?: TypeNumber;  

- errorCorrectionLevel?: ErrorCorrectionLevel;

- data?: string;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值