@nuintun/qrcode
Version:
A pure JavaScript QRCode encode and decode library.
174 lines (170 loc) • 6.56 kB
JavaScript
/**
* @module QRCode
* @package @nuintun/qrcode
* @license MIT
* @version 5.0.2
* @author nuintun <nuintun@qq.com>
* @description A pure JavaScript QRCode encode and decode library.
* @see https://github.com/nuintun/qrcode#readme
*/
import { histogram } from './histogram.js';
import { BitMatrix } from '../common/BitMatrix.js';
/**
* @module index
*/
const BLOCK_SIZE_POWER = 3;
const MIN_DYNAMIC_RANGE = 24;
const BLOCK_SIZE = 1 << BLOCK_SIZE_POWER;
const BLOCK_SIZE_MASK = BLOCK_SIZE - 1;
const MINIMUM_DIMENSION = BLOCK_SIZE * 5;
function calculateSubSize(size) {
let subSize = size >> BLOCK_SIZE_POWER;
if (size & BLOCK_SIZE_MASK) {
subSize++;
}
return subSize;
}
function clamp(value, max) {
return value < 2 ? 2 : Math.min(value, max);
}
function calculateOffset(offset, max) {
offset = offset << BLOCK_SIZE_POWER;
return offset > max ? max : offset;
}
function calculateBlackPoints(luminances, width, height) {
const blackPoints = [];
const maxOffsetX = width - BLOCK_SIZE;
const maxOffsetY = height - BLOCK_SIZE;
const subWidth = calculateSubSize(width);
const subHeight = calculateSubSize(height);
for (let y = 0; y < subHeight; y++) {
blackPoints[y] = new Int32Array(subWidth);
const offsetY = calculateOffset(y, maxOffsetY);
for (let x = 0; x < subWidth; x++) {
let sum = 0;
let max = 0;
let min = 0xff;
const offsetX = calculateOffset(x, maxOffsetX);
for (let y1 = 0, offset = offsetY * width + offsetX; y1 < BLOCK_SIZE; y1++, offset += width) {
for (let x1 = 0; x1 < BLOCK_SIZE; x1++) {
const luminance = luminances[offset + x1];
sum += luminance;
// still looking for good contrast.
if (luminance < min) {
min = luminance;
}
if (luminance > max) {
max = luminance;
}
}
// short-circuit min/max tests once dynamic range is met.
if (max - min > MIN_DYNAMIC_RANGE) {
// finish the rest of the rows quickly.
for (y1++, offset += width; y1 < BLOCK_SIZE; y1++, offset += width) {
for (let x1 = 0; x1 < BLOCK_SIZE; x1++) {
sum += luminances[offset + x1];
}
}
}
}
// The default estimate is the average of the values in the block.
let average = sum >> (BLOCK_SIZE_POWER * 2);
if (max - min <= MIN_DYNAMIC_RANGE) {
// If variation within the block is low, assume this is a block with only light or only
// dark pixels. In that case we do not want to use the average, as it would divide this
// low contrast area into black and white pixels, essentially creating data out of noise.
//
// The default assumption is that the block is light/background. Since no estimate for
// the level of dark pixels exists locally, use half the min for the block.
average = min / 2;
if (y > 0 && x > 0) {
// Correct the "white background" assumption for blocks that have neighbors by comparing
// the pixels in this block to the previously calculated black points. This is based on
// the fact that dark barcode symbology is always surrounded by some amount of light
// background for which reasonable black point estimates were made. The bp estimated at
// the boundaries is used for the interior.
// The (min < bp) is arbitrary but works better than other heuristics that were tried.
const averageNeighborBlackPoint = (blackPoints[y - 1][x] + 2 * blackPoints[y][x - 1] + blackPoints[y - 1][x - 1]) / 4;
if (min < averageNeighborBlackPoint) {
average = averageNeighborBlackPoint;
}
}
}
blackPoints[y][x] = average;
}
}
return blackPoints;
}
function adaptiveThreshold(luminances, width, height) {
const maxOffsetX = width - BLOCK_SIZE;
const maxOffsetY = height - BLOCK_SIZE;
const subWidth = calculateSubSize(width);
const subHeight = calculateSubSize(height);
const matrix = new BitMatrix(width, height);
const blackPoints = calculateBlackPoints(luminances, width, height);
for (let y = 0; y < subHeight; y++) {
const top = clamp(y, subHeight - 3);
const offsetY = calculateOffset(y, maxOffsetY);
for (let x = 0; x < subWidth; x++) {
let sum = 0;
const left = clamp(x, subWidth - 3);
const offsetX = calculateOffset(x, maxOffsetX);
for (let z = -2; z <= 2; z++) {
const blackRow = blackPoints[top + z];
sum += blackRow[left - 2] + blackRow[left - 1] + blackRow[left] + blackRow[left + 1] + blackRow[left + 2];
}
const average = sum / 25;
for (let y = 0, offset = offsetY * width + offsetX; y < BLOCK_SIZE; y++, offset += width) {
for (let x = 0; x < BLOCK_SIZE; x++) {
// Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0.
if (luminances[offset + x] <= average) {
matrix.set(offsetX + x, offsetY + y);
}
}
}
}
}
return matrix;
}
/**
* @function grayscale
* @description Convert an image to grayscale.
* @param image The image data to convert.
*/
function grayscale({ data, width, height }) {
// Convert image to grayscale.
const luminances = new Uint8Array(width * height);
for (let y = 0; y < height; y++) {
const offset = y * width;
for (let x = 0; x < width; x++) {
const index = offset + x;
const colorIndex = index * 4;
const r = data[colorIndex];
const g = data[colorIndex + 1];
const b = data[colorIndex + 2];
// 0.299R + 0.587G + 0.114B (YUV/YIQ for PAL and NTSC),
// (R * 306) >> 10 is approximately equal to R * 0.299, and so on.
// 0x200 >> 10 is 0.5, it implements rounding.
luminances[offset + x] = (r * 306 + g * 601 + b * 117 + 0x200) >> 10;
}
}
return luminances;
}
/**
* @function binarize
* @description Convert the image to a binary matrix.
* @param luminances The luminances of the image.
* @param width The width of the image.
* @param height The height of the image.
*/
function binarize(luminances, width, height) {
if (luminances.length !== width * height) {
throw new Error('luminances length must be equals to width * height');
}
if (width < MINIMUM_DIMENSION || height < MINIMUM_DIMENSION) {
return histogram(luminances, width, height);
} else {
return adaptiveThreshold(luminances, width, height);
}
}
export { binarize, grayscale };