Published
Edited
Mar 9, 2021
Insert cell
md`# QR Code`
Insert cell
d3 = require('d3')
Insert cell
import { getSVG, drawRect } from '@nuuuwan/svg-utils'
Insert cell
import { div, pow, smul, pad, xor, numToP } from '@nuuuwan/polynomials'
Insert cell
QUIET_ZONE_SIZE = 4
Insert cell
COLOR = Object({
QUIET_ZONE: d3.hsl(120, 1, 0.5),
POSITION_BOX: d3.hsl(240, 1, 0.5),
SEPERATOR: d3.hsl(180, 1, 0.5),
ALIGNMENT: d3.hsl(300, 1, 0.5),
TIMING: d3.hsl(30, 1, 0.5),
FORMAT_INFO: d3.hsl(330, 1, 0.5),
VERSION_INFO: d3.hsl(0, 1, 0.5),
})
Insert cell
ERROR_CORRECTION_INFO = [
{ level: 'M', bits: [0, 0] },
{ level: 'L', bits: [0, 1] },
{ level: 'H', bits: [1, 0] },
{ level: 'Q', bits: [1, 1] }
]
Insert cell
MASK_INFO = [
{ bits: [0, 0, 0], condition: (i, j) => (i + j) % 2 === 0 },
{ bits: [0, 0, 1], condition: (i, j) => i % 2 === 0 },
{ bits: [0, 1, 0], condition: (i, j) => j % 3 === 0 },
{ bits: [0, 1, 1], condition: (i, j) => (i + j) % 3 === 0 },

{
bits: [1, 0, 0],
condition: (i, j) => (Math.trunc(i / 2) + Math.trunc(j / 3)) % 2 === 0
},
{ bits: [1, 0, 1], condition: (i, j) => ((i * j) % 2) + ((i * j) % 3) === 0 },
{
bits: [1, 1, 0],
condition: (i, j) => (((i * j) % 2) + ((i * j) % 3)) % 2 === 0
},
{
bits: [1, 1, 1],
condition: (i, j) => (((i * j) % 3) + ((i + j) % 2)) % 2 === 0
}
]
Insert cell
MODE_INDICATORS = Object({
ECI: [0, 1, 1, 1],
Numeric: [0, 0, 0, 1],
Alphanumeric: [0, 0, 1, 0],
_8BitByte: [0, 1, 0, 0],
Kanji: [1, 0, 0, 0],
StructuredAppend: [0, 0, 1, 1],
FNC11: [0, 1, 0, 1],
FNC12: [1, 0, 0, 1],
Terminator: [0, 0, 0, 0]
})
Insert cell
function getAlignmentPatternCount(qrVersion) {
const x = 0;
if (qrVersion <= 0) {
return undefined;
} else if (qrVersion <= 1) {
return 0;
} else if (qrVersion <= 6) {
return 2;
} else if (qrVersion <= 13) {
return 3;
} else if (qrVersion <= 20) {
return 4;
} else if (qrVersion <= 27) {
return 5;
} else if (qrVersion <= 34) {
return 6;
} else if (qrVersion <= 40) {
return 7;
} else {
return undefined;
}
}
Insert cell
function renderQRCode(qrVersion = 1) {
const maxSize = 300;
const svg = getSVG({ width: maxSize, height: maxSize });
const dim = 4 * qrVersion + 17 + QUIET_ZONE_SIZE * 2;
const rectSize = parseInt(maxSize / dim);

let data = d3.range(0, dim).map(function(i) {
return d3.range(0, dim).map(function(j) {
return ['gray', Math.random() < 0.5 ? 0 : 1];
});
});

function drawQRLine([i1, j1], [i2, j2], getColor) {
d3.range(i1, i2).map(function(i) {
d3.range(j1, j2).map(function(j) {
data[i][j] = getColor(i, j);
});
});
}

function drawQRRect([i, j], [rectWidth, rectHeight], getColor, strokeWidth) {
drawQRLine([i, j], [i + strokeWidth, j + rectHeight], getColor);
drawQRLine([i, j], [i + rectWidth, j + strokeWidth], getColor);

drawQRLine(
[i + rectWidth - strokeWidth, j],
[i + rectWidth, j + rectHeight],
getColor
);
drawQRLine(
[i, j + rectHeight - strokeWidth],
[i + rectWidth, j + rectHeight],
getColor
);
}

function drawQRSquaare([i, j], squareSize, getColor, strokeWidth) {
drawQRRect([i, j], [squareSize, squareSize], getColor, strokeWidth);
}

// Quiet Zone
drawQRRect([0, 0], [dim, dim], () => [COLOR.QUIET_ZONE, 0], QUIET_ZONE_SIZE);

// Position Detection Patterns (3) & Seperators for Position Detection Patterns (3)
const positionBoxGap = dim - QUIET_ZONE_SIZE * 2 - 7;
[[0, 0], [1, 0], [0, 1]].forEach(function([bi, bj]) {
const squareSize = 7;
const [i0, j0] = [
QUIET_ZONE_SIZE + bi * (positionBoxGap - 1) + 4 - (squareSize + 1) / 2,
QUIET_ZONE_SIZE + bj * (positionBoxGap - 1) + 4 - (squareSize + 1) / 2
];
drawQRSquaare([i0, j0], squareSize + 1, () => [COLOR.SEPERATOR, 0], 1);

[1, 3, 5, 7].forEach(function(squareSize) {
const [i0, j0] = [
QUIET_ZONE_SIZE + bi * positionBoxGap + 4 - (squareSize + 1) / 2,
QUIET_ZONE_SIZE + bj * positionBoxGap + 4 - (squareSize + 1) / 2
];
drawQRSquaare(
[i0, j0],
squareSize,
() => [COLOR.POSITION_BOX, squareSize === 5 ? 0 : 1],
1
);
});
});

const innerSize = dim - QUIET_ZONE_SIZE * 2 - 7 * 2 + 1;
const nAlignment = getAlignmentPatternCount(qrVersion);

d3.range(0, nAlignment).map(function(bi) {
d3.range(0, nAlignment).map(function(bj) {
if (
(bi === 0 && bj === 0) ||
(bi === 0 && bj === nAlignment - 1) ||
(bi === nAlignment - 1 && bj === 0)
) {
return;
}

[1, 3, 5].forEach(function(squareSize) {
const [i0, j0] = [
QUIET_ZONE_SIZE +
7 +
(innerSize * bi) / (nAlignment - 1) -
(squareSize + 1) / 2,
QUIET_ZONE_SIZE +
7 +
(innerSize * bj) / (nAlignment - 1) -
(squareSize + 1) / 2
];
drawQRSquaare(
[i0, j0],
squareSize,
() => [COLOR.ALIGNMENT, squareSize === 3 ? 0 : 1],
1
);
});
});
});

// Timing Patterns
const spanTiming = dim - QUIET_ZONE_SIZE * 2 - 17;
[[0, 1], [1, 0]].forEach(function([bi, bj]) {
const [i0, j0] = [
QUIET_ZONE_SIZE + 6 + bi * 2,
QUIET_ZONE_SIZE + 6 + bj * 2
];
drawQRLine(
[i0, j0],
[i0 + spanTiming * bi + 1, j0 + spanTiming * bj + 1],
(i, j) => [COLOR.TIMING, (i + j + 1) % 2]
);
});

// Format Information
const eclBits = [0, 0];
const maskBits = [1, 0, 1];
const dataBits = [].concat(eclBits, maskBits);
const gX10 = [1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1];
const bchR = div(pow(dataBits, 10), gX10)[1];
const totalBits = [].concat(dataBits, pad(smul(bchR, -1), 10));
const mask = [1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0];
const formatBits = xor(totalBits, mask);

drawQRLine(
[QUIET_ZONE_SIZE + 8, QUIET_ZONE_SIZE],
[QUIET_ZONE_SIZE + 9, QUIET_ZONE_SIZE + 6],
(i, j) => [COLOR.FORMAT_INFO, formatBits[j - QUIET_ZONE_SIZE]]
);
drawQRLine(
[QUIET_ZONE_SIZE + 8, QUIET_ZONE_SIZE + 7],
[QUIET_ZONE_SIZE + 9, QUIET_ZONE_SIZE + 9],
(i, j) => [COLOR.FORMAT_INFO, formatBits[j - QUIET_ZONE_SIZE]]
);
drawQRLine(
[QUIET_ZONE_SIZE + 7, QUIET_ZONE_SIZE + 8],
[QUIET_ZONE_SIZE + 8, QUIET_ZONE_SIZE + 9],
(i, j) => [COLOR.FORMAT_INFO, formatBits[9]]
);
drawQRLine(
[QUIET_ZONE_SIZE, QUIET_ZONE_SIZE + 8],
[QUIET_ZONE_SIZE + 6, QUIET_ZONE_SIZE + 9],
(i, j) => [COLOR.FORMAT_INFO, formatBits[14 - (i - QUIET_ZONE_SIZE)]]
);
drawQRLine(
[dim - QUIET_ZONE_SIZE - 9, QUIET_ZONE_SIZE + 8],
[dim - QUIET_ZONE_SIZE, QUIET_ZONE_SIZE + 9],
(i, j) => [COLOR.FORMAT_INFO, formatBits[dim - QUIET_ZONE_SIZE - 1 - i]]
);
drawQRLine(
[QUIET_ZONE_SIZE + 8, dim - QUIET_ZONE_SIZE - 9],
[QUIET_ZONE_SIZE + 9, dim - QUIET_ZONE_SIZE],
(i, j) => [COLOR.FORMAT_INFO, formatBits[j - dim + QUIET_ZONE_SIZE + 15]]
);

// Version Information
const versionDataBits = pad(numToP(qrVersion), 6);
const gX12 = [1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1];
const bchRVersion = div(pow(versionDataBits, 12), gX12)[1];
const versionBits = [].concat(
versionDataBits,
pad(smul(bchRVersion, -1), 12)
);

drawQRLine(
[QUIET_ZONE_SIZE, dim - QUIET_ZONE_SIZE - 8 - 3],
[QUIET_ZONE_SIZE + 6, dim - QUIET_ZONE_SIZE - 8],
(i, j) => [
COLOR.VERSION_INFO,
versionBits[
(i - QUIET_ZONE_SIZE) * 3 + (j - (dim - QUIET_ZONE_SIZE - 8 - 3))
]
]
);

drawQRLine(
[dim - QUIET_ZONE_SIZE - 8 - 3, QUIET_ZONE_SIZE],
[dim - QUIET_ZONE_SIZE - 8, QUIET_ZONE_SIZE + 6],
(i, j) => [
COLOR.VERSION_INFO,
versionBits[
(j - QUIET_ZONE_SIZE) * 3 + (i - (dim - QUIET_ZONE_SIZE - 8 - 3))
]
]
);

// Data
const dataNumber = 123;
const dataNumberBits = pad(numToP(dataNumber), 10);
const charCountBits = pad(numToP(dataNumber.toString().length), 10);
const payloadDataBits = [].concat(
MODE_INDICATORS.Numeric,
charCountBits,
dataNumberBits,
MODE_INDICATORS.Terminator
);
console.debug('dataBits', payloadDataBits);

// Finalize
const IS_TEST_MODE = true;
d3.range(0, dim).map(function(i) {
d3.range(0, dim).map(function(j) {
const [x, y] = [i * rectSize, j * rectSize];
const [color, value] = data[i][j];
const style = IS_TEST_MODE
? {
fill: color,
stroke: 'white',
fillOpacity: 0.2 + value * 0.8
}
: { fill: 'black', stroke: 'none', fillOpacity: value };
drawRect(svg, [x, y], [rectSize, rectSize], style);
});
});

return svg.node();
}
Insert cell
renderQRCode(2)
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more