1.前言
涉及js库和技术
- qrcode-generator 链接
- 画布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 链接
对于二维码的生成,大家基本都是使用qrcdoejs。
使用 new QRCode(element, option)
参数说明
名称 | 默认值 | 说明 |
element | - | 显示二维码的元素或该元素的 ID |
option | 参数 |
option参数说明
名称 | 默认值 | 说明 |
width | 256 | 图像宽度 |
height | 256 | 图像高度 |
typeNumber | 4 | |
colorDark | "#000000" | 前景色 |
colorLight | "#ffffff" | 背景色 |
correctLevel | QRCode.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;