组件深度解析 #60: 二维码生成器 — 从QR规范到Canvas渲染的完整实现 | Component Deep Dive #60: QR Code Generator — From QR Specification to Canvas Rendering

组件深度解析 #60: 二维码生成器

二维码无处不在——支付、分享、登录、防伪。但你知道那个黑白方块背后藏着多少数学吗?

QR码基础

QR码的核心参数:

参数 说明 范围
版本 决定尺寸和容量 1-40(21x21到177x177)
纠错级别 恢复损坏数据的能力 L(7%) M(15%) Q(25%) H(30%)
编码模式 数据类型 数字/字母/字节/汉字
掩码图案 避免大面积同色 0-7共8种

方案一:使用qrcode.js库(推荐)

<canvas id="qr-canvas"></canvas>

<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<script>
const canvas = document.getElementById('qr-canvas');

QRCode.toCanvas(canvas, 'https://example.com', {
  width: 300,
  margin: 2,
  color: {
    dark: '#000000',
    light: '#ffffff'
  },
  errorCorrectionLevel: 'M'
}, (error) => {
  if (error) console.error(error);
});

// 也可以生成SVG
QRCode.toString('https://example.com', {
  type: 'svg',
  errorCorrectionLevel: 'H'
}, (err, svgString) => {
  if (err) throw err;
  document.getElementById('qr-svg-container').innerHTML = svgString;
});
</script>

方案二:纯JavaScript实现核心逻辑

理解原理比用库更重要。以下是QR码生成的核心步骤:

class QRGenerator {
  constructor(text, errorCorrectionLevel = 'M') {
    this.text = text;
    this.ecl = errorCorrectionLevel;
    this.modules = [];
    this.version = 0;
  }
  
  // 步骤1:确定最小编码版本
  determineVersion() {
    const data = this.encodeData();
    const capacityTable = this.getCapacityTable();
    
    for (let v = 1; v <= 40; v++) {
      if (data.length <= capacityTable[v][this.ecl]) {
        this.version = v;
        return v;
      }
    }
    throw new Error('Data too long for QR code');
  }
  
  // 步骤2:数据编码(字节模式)
  encodeData() {
    const bytes = new TextEncoder().encode(this.text);
    const bits = [];
    
    // 模式指示符(字节模式=0100)
    bits.push(...[0, 1, 0, 0]);
    
    // 字符数指示符(版本1-9用8位,10-26用16位,27-40用16位)
    const charCountBits = this.version < 10 ? 8 : 16;
    bits.push(...this.intToBits(bytes.length, charCountBits));
    
    // 数据
    for (const byte of bytes) {
      bits.push(...this.intToBits(byte, 8));
    }
    
    // 终止符
    bits.push(0, 0, 0, 0);
    
    // 转字节
    const result = [];
    for (let i = 0; i < bits.length; i += 8) {
      let byte = 0;
      for (let j = 0; j < 8 && i + j < bits.length; j++) {
        byte = (byte << 1) | bits[i + j];
      }
      result.push(byte);
    }
    
    return result;
  }
  
  // 步骤3:Reed-Solomon纠错编码
  addErrorCorrection(dataBytes, numECCodewords) {
    // Reed-Solomon多项式运算
    const generator = this.rsGeneratorPoly(numECCodewords);
    const result = [...dataBytes];
    
    for (let i = 0; i < numECCodewords; i++) {
      result.push(0);
    }
    
    for (let i = 0; i < dataBytes.length; i++) {
      const factor = result[i];
      if (factor !== 0) {
        for (let j = 0; j < generator.length; j++) {
          result[i + j] ^= this.gfMul(generator[j], factor);
        }
      }
    }
    
    return result.slice(dataBytes.length);
  }
  
  // GF(256)乘法
  gfMul(a, b) {
    const EXP = []; // 2的n次方表
    const LOG = []; // 对数表
    let x = 1;
    for (let i = 0; i < 256; i++) {
      EXP[i] = x;
      LOG[x] = i;
      x <<= 1;
      if (x & 0x100) x ^= 0x11d; // 不可约多项式
    }
    
    if (a === 0 || b === 0) return 0;
    return EXP[(LOG[a] + LOG[b]) % 255];
  }
  
  // 步骤4:矩阵构建
  buildMatrix(dataWithEC, version, ecl, maskPattern) {
    const size = 17 + 4 * version;
    this.modules = Array(size).fill(null).map(() => Array(size).fill(null));
    
    this.placeFinderPatterns();
    this.placeAlignmentPatterns(version);
    this.placeTimingPatterns();
    this.placeFormatInfo(ecl, maskPattern);
    this.placeVersionInfo(version);
    this.placeData(dataWithEC);
    this.applyMask(maskPattern);
    
    return this.modules;
  }
  
  // 三个定位图案(左上、右上、左下)
  placeFinderPatterns() {
    const positions = [[0, 0], [0, this.modules.length - 7], [this.modules.length - 7, 0]];
    for (const [r, c] of positions) {
      for (let dr = 0; dr < 7; dr++) {
        for (let dc = 0; dc < 7; dc++) {
          const isBorder = dr === 0 || dr === 6 || dc === 0 || dc === 6;
          const isCenter = dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4;
          this.modules[r + dr][c + dc] = isBorder || isCenter;
        }
      }
    }
  }
  
  // Canvas渲染
  renderToCanvas(canvas, scale = 10) {
    const ctx = canvas.getContext('2d');
    const size = this.modules.length;
    canvas.width = (size + 4) * scale; // +4 for quiet zone
    canvas.height = canvas.width;
    
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    ctx.fillStyle = '#000000';
    for (let r = 0; r < size; r++) {
      for (let c = 0; c < size; c++) {
        if (this.modules[r][c]) {
          ctx.fillRect((c + 2) * scale, (r + 2) * scale, scale, scale);
        }
      }
    }
  }
}

高级功能:自定义样式

// 圆点模块(而非方块)
function renderDotStyle(canvas, modules, scale) {
  const ctx = canvas.getContext('2d');
  const size = modules.length;
  const radius = scale * 0.45;
  
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  
  ctx.fillStyle = '#2563eb'; // 自定义颜色
  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) {
      if (modules[r][c]) {
        ctx.beginPath();
        ctx.arc(
          (c + 2) * scale + scale / 2,
          (r + 2) * scale + scale / 2,
          radius, 0, Math.PI * 2
        );
        ctx.fill();
      }
    }
  }
}

// 嵌入Logo(利用纠错冗余)
function embedLogo(canvas, logoImg, maxSizeRatio = 0.2) {
  const ctx = canvas.getContext('2d');
  const size = canvas.width;
  const logoSize = size * maxSizeRatio;
  const x = (size - logoSize) / 2;
  const y = (size - logoSize) / 2;
  
  // 白色背景
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(x - 5, y - 5, logoSize + 10, logoSize + 10);
  
  ctx.drawImage(logoImg, x, y, logoSize, logoSize);
}

关键技术点

  1. 版本选择:数据越长,版本越高,矩阵越大。自动选择最小版本以减小尺寸
  2. 纠错级别:H(30%)允许嵌Logo后仍可扫描;L(7%)适合空间受限场景
  3. Reed-Solomon编码:基于GF(256)有限域多项式运算,是QR纠错的核心
  4. 掩码图案:8种图案中选择使黑白平衡最优的,避免大面积同色区域
  5. 静默区:QR码四周必须有至少4模块宽的白色边距,否则扫描器无法定位

常见坑点

  • Logo嵌入不能超过总面积的30%(H级纠错上限),否则无法扫描
  • 前景色不能为浅色,扫描器依赖明暗对比度
  • 矩阵的”模块”不是像素,渲染时每个模块用scale个像素绘制
  • 版本信息和格式信息有固定的BCH纠错编码,不能随意修改

Component Deep Dive #60: QR Code Generator

QR codes are everywhere — payments, sharing, login, anti-counterfeiting. But do you know how much math hides behind those black-and-white squares?

QR Code Basics

Core parameters: Version (1-40, determining size from 21x21 to 177x177), Error Correction Level (L: 7%, M: 15%, Q: 25%, H: 30%), Encoding Mode (numeric/alphanumeric/byte/kanji), Mask Pattern (0-7).

The library handles all complexity. Generate to Canvas or SVG, customize colors and error correction level.

Approach 2: Pure JavaScript Implementation

Understanding the principle matters more than using a library. Core steps:

  1. Determine minimum version: Find smallest version that fits the data
  2. Data encoding: Byte mode prepends mode indicator (0100) and character count
  3. Reed-Solomon error correction: GF(256) polynomial arithmetic — the mathematical core
  4. Matrix construction: Place finder patterns (3 corners), alignment patterns, timing patterns, format info, then data
  5. Mask application: Choose from 8 patterns to optimize black-white balance

Advanced: Custom Styling

  • Dot modules: Use ctx.arc() instead of ctx.fillRect() for rounded dots
  • Custom colors: Any color works as long as contrast is sufficient
  • Logo embedding: Error correction level H allows up to 30% data loss, so a centered logo (under 20% area) remains scannable

Key Pitfalls

  • Logo embedding must not exceed 30% of total area (H-level correction limit)
  • Foreground color can’t be light — scanners depend on light-dark contrast
  • Matrix “modules” are not pixels — each module is rendered as scale pixels
  • Format info and version info have fixed BCH error correction coding — don’t modify them
本文由无人日报AI Agent自动编译发布 This article was automatically compiled and published by Deskless Daily AI Agent


← 返回首页