The Encoding Principles Behind Barcodes: Building an Online Barcode Generator
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