The Encoding Principles Behind Barcodes: Building an Online Barcode Generator#

Recently added a barcode generation feature to a project. Turns out those black-and-white stripes have some interesting encoding rules underneath.

What is a Barcode?#

A barcode converts numbers or letters into alternating black and white stripes. When a scanner reads it, black bars absorb light while white spaces reflect it, generating electrical signals that decode back to the original content.

Most common formats:

  • CODE128: Universal format supporting numbers, letters, symbols. Widely used in logistics.
  • EAN-13: 13-digit numbers for retail products, globally standardized.
  • UPC: North American product barcode, 12 digits.
  • CODE39: Industrial standard supporting uppercase letters + numbers + some symbols.
  • ITF-14: Logistics packaging barcode, 14 digits.

Canvas-Based Barcode Rendering#

The core uses the JsBarcode library, but understanding what it does under the hood is useful.

1. Encoding Mapping#

Each barcode format has its own encoding table. Here’s CODE128 as an example:

// CODE128 encoding table (partial)
const CODE128_PATTERNS = {
  ' ': '11011001100',  // space
  '!': '11001101100',  // exclamation
  '"': '11001100110',  // double quote
  // ... more characters
  '0': '10011100110',  // digit 0
  '1': '11001110010',  // digit 1
  // ...
}

function encodeCODE128(text: string): string {
  let pattern = '11010000100'  // Start Code B

  for (const char of text) {
    pattern += CODE128_PATTERNS[char] || ''
  }

  // Calculate checksum
  let checksum = 104  // Start Code B value
  for (let i = 0; i < text.length; i++) {
    const value = CODE128_VALUES[text[i]] || 0
    checksum += (i + 1) * value
  }
  pattern += CODE128_PATTERNS[checksum % 103]

  pattern += '1100011101011'  // Stop

  return pattern
}

Each character maps to an 11-bit binary string. 1 means black bar, 0 means white space.

2. Canvas Rendering#

Once you have the binary pattern, render it with Canvas:

function drawBarcode(
  ctx: CanvasRenderingContext2D,
  pattern: string,
  options: { width: number; height: number; margin: number }
) {
  const { width, height, margin } = options
  const barWidth = width / pattern.length

  ctx.fillStyle = '#ffffff'
  ctx.fillRect(0, 0, width + margin * 2, height + margin * 2)

  ctx.fillStyle = '#000000'
  for (let i = 0; i < pattern.length; i++) {
    if (pattern[i] === '1') {
      ctx.fillRect(
        margin + i * barWidth,
        margin,
        barWidth,
        height
      )
    }
  }
}

Simple as that! But real-world usage has more details.

Input Restrictions by Format#

Different formats have strict input requirements:

EAN-13 Check Digit Calculation#

EAN-13 accepts 12 digits, with the 13th being a check digit:

function calculateEAN13CheckDigit(data: string): string {
  if (data.length !== 12 || !/^\d{12}$/.test(data)) {
    throw new Error('EAN-13 requires 12 digits')
  }

  let sum = 0
  for (let i = 0; i < 12; i++) {
    sum += parseInt(data[i]) * (i % 2 === 0 ? 1 : 3)
  }

  const checkDigit = (10 - (sum % 10)) % 10
  return data + checkDigit
}

// Example
calculateEAN13CheckDigit('690123456789')  // "6901234567892"

Weighted sum: odd positions × 1, even positions × 3, then calculate the complement.

CODE39 Character Set Limitation#

CODE39 only supports these characters:

const CODE39_CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%'

function validateCODE39(text: string): boolean {
  return /^[A-Z0-9\-. $/+%]+$/.test(text)
}

Lowercase letters need to be converted to uppercase first.

ITF-14 Even-Digit Requirement#

ITF-14 requires an even number of digits, as it uses two black bars + two white bars per digit:

function encodeITF14(data: string): string {
  if (data.length % 2 !== 0) {
    throw new Error('ITF-14 requires even number of digits')
  }

  const PAIRS = {
    '00': '00110', '01': '10001', '02': '01001',
    // ... all 00-99 combinations
  }

  let pattern = ''
  for (let i = 0; i < data.length; i += 2) {
    pattern += PAIRS[data.substr(i, 2)]
  }

  return pattern
}

Practical Pitfalls#

1. Auto-Width Adjustment#

Users input varying lengths, so barcode width should adapt:

const minWidth = 100  // minimum width
const maxWidth = 400  // maximum width
const barCount = pattern.length
const idealWidth = barCount * 2  // minimum 2px per bar

const actualWidth = Math.min(maxWidth, Math.max(minWidth, idealWidth))

2. Resolution Adaptation#

Printing needs high resolution, but screen display uses low resolution:

function downloadBarcode(canvas: HTMLCanvasElement, filename: string, dpi = 300) {
  const printCanvas = document.createElement('canvas')
  const scale = dpi / 96  // screen is 96 DPI

  printCanvas.width = canvas.width * scale
  printCanvas.height = canvas.height * scale

  const ctx = printCanvas.getContext('2d')!
  ctx.scale(scale, scale)
  ctx.drawImage(canvas, 0, 0)

  const link = document.createElement('a')
  link.download = filename
  link.href = printCanvas.toDataURL('image/png')
  link.click()
}

3. Quiet Zone#

Barcodes need blank margins on both sides, otherwise scanners can’t read them:

const QUIET_ZONE_WIDTH = 10  // at least 10x bar width

function drawBarcodeWithQuietZone(ctx, pattern, width, height) {
  const quietWidth = Math.max(10, width * 0.1)
  const barcodeWidth = width - quietWidth * 2

  drawBarcode(ctx, pattern, {
    x: quietWidth,
    width: barcodeWidth,
    height
  })
}

Complete Implementation#

Based on these principles, I built: Barcode Generator

Key features:

  • 8 mainstream formats (CODE128, EAN-13, EAN-8, UPC, CODE39, ITF-14, MSI, Pharmacode)
  • Real-time preview with parameter adjustment (width, height, font size)
  • Automatic input validation
  • High-resolution PNG download

The core code is just dozens of lines, but handling edge cases takes effort. Hope this helps.


Related: QR Code Generator | Color Extractor