130 lines
3.5 KiB
TypeScript
130 lines
3.5 KiB
TypeScript
import { deflateSync } from "node:zlib";
|
|
import QRCodeModule from "qrcode-terminal/vendor/QRCode/index.js";
|
|
import QRErrorCorrectLevelModule from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js";
|
|
|
|
type QRCodeConstructor = new (
|
|
typeNumber: number,
|
|
errorCorrectLevel: unknown,
|
|
) => {
|
|
addData: (data: string) => void;
|
|
make: () => void;
|
|
getModuleCount: () => number;
|
|
isDark: (row: number, col: number) => boolean;
|
|
};
|
|
|
|
const QRCode = QRCodeModule as unknown as QRCodeConstructor;
|
|
const QRErrorCorrectLevel = QRErrorCorrectLevelModule as Record<string, unknown>;
|
|
|
|
function createQrMatrix(input: string) {
|
|
const qr = new QRCode(-1, QRErrorCorrectLevel.L);
|
|
qr.addData(input);
|
|
qr.make();
|
|
return qr;
|
|
}
|
|
|
|
function fillPixel(
|
|
buf: Buffer,
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
r: number,
|
|
g: number,
|
|
b: number,
|
|
a = 255,
|
|
) {
|
|
const idx = (y * width + x) * 4;
|
|
buf[idx] = r;
|
|
buf[idx + 1] = g;
|
|
buf[idx + 2] = b;
|
|
buf[idx + 3] = a;
|
|
}
|
|
|
|
function crcTable() {
|
|
const table = new Uint32Array(256);
|
|
for (let i = 0; i < 256; i += 1) {
|
|
let c = i;
|
|
for (let k = 0; k < 8; k += 1) {
|
|
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
}
|
|
table[i] = c >>> 0;
|
|
}
|
|
return table;
|
|
}
|
|
|
|
const CRC_TABLE = crcTable();
|
|
|
|
function crc32(buf: Buffer) {
|
|
let crc = 0xffffffff;
|
|
for (let i = 0; i < buf.length; i += 1) {
|
|
crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
|
}
|
|
return (crc ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function pngChunk(type: string, data: Buffer) {
|
|
const typeBuf = Buffer.from(type, "ascii");
|
|
const len = Buffer.alloc(4);
|
|
len.writeUInt32BE(data.length, 0);
|
|
const crc = crc32(Buffer.concat([typeBuf, data]));
|
|
const crcBuf = Buffer.alloc(4);
|
|
crcBuf.writeUInt32BE(crc, 0);
|
|
return Buffer.concat([len, typeBuf, data, crcBuf]);
|
|
}
|
|
|
|
function encodePngRgba(buffer: Buffer, width: number, height: number) {
|
|
const stride = width * 4;
|
|
const raw = Buffer.alloc((stride + 1) * height);
|
|
for (let row = 0; row < height; row += 1) {
|
|
const rawOffset = row * (stride + 1);
|
|
raw[rawOffset] = 0; // filter: none
|
|
buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride);
|
|
}
|
|
const compressed = deflateSync(raw);
|
|
|
|
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
const ihdr = Buffer.alloc(13);
|
|
ihdr.writeUInt32BE(width, 0);
|
|
ihdr.writeUInt32BE(height, 4);
|
|
ihdr[8] = 8; // bit depth
|
|
ihdr[9] = 6; // color type RGBA
|
|
ihdr[10] = 0; // compression
|
|
ihdr[11] = 0; // filter
|
|
ihdr[12] = 0; // interlace
|
|
|
|
return Buffer.concat([
|
|
signature,
|
|
pngChunk("IHDR", ihdr),
|
|
pngChunk("IDAT", compressed),
|
|
pngChunk("IEND", Buffer.alloc(0)),
|
|
]);
|
|
}
|
|
|
|
export async function renderQrPngBase64(
|
|
input: string,
|
|
opts: { scale?: number; marginModules?: number } = {},
|
|
): Promise<string> {
|
|
const { scale = 6, marginModules = 4 } = opts;
|
|
const qr = createQrMatrix(input);
|
|
const modules = qr.getModuleCount();
|
|
const size = (modules + marginModules * 2) * scale;
|
|
|
|
const buf = Buffer.alloc(size * size * 4, 255);
|
|
for (let row = 0; row < modules; row += 1) {
|
|
for (let col = 0; col < modules; col += 1) {
|
|
if (!qr.isDark(row, col)) continue;
|
|
const startX = (col + marginModules) * scale;
|
|
const startY = (row + marginModules) * scale;
|
|
for (let y = 0; y < scale; y += 1) {
|
|
const pixelY = startY + y;
|
|
for (let x = 0; x < scale; x += 1) {
|
|
const pixelX = startX + x;
|
|
fillPixel(buf, pixelX, pixelY, size, 0, 0, 0, 255);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const png = encodePngRgba(buf, size, size);
|
|
return png.toString("base64");
|
|
}
|