/**
* QR rendering for both hosts: half-block Unicode lines for the terminal, and
* an SVG data URI for the desktop web host, which must not draw shapes with
* cell characters and would render them with unscannable line gaps anyway.
*
* A QR code must be dark-on-light for phone cameras, so both outputs have to be
* painted with a dark foreground on a light background whatever the theme is.
*/
import qrcode from "qrcode-generator";
/** Mandatory light border around the code: the QR spec requires 4 modules on every side. */
const QUIET_ZONE_MODULES = 4;
const BOTH_LIGHT = " ";
const TOP_DARK = "\u2580"; // ▀
const BOTTOM_DARK = "\u2584"; // ▄
const BOTH_DARK = "\u2588"; // █
/** Quiet zone included, so callers only deal with one coordinate space. */
function qrGrid(text: string): { size: number; isDark: (row: number, col: number) => boolean } {
const qr = qrcode(0, "M");
qr.addData(text);
qr.make();
const moduleCount = qr.getModuleCount();
return {
size: moduleCount + QUIET_ZONE_MODULES * 2,
isDark: (row: number, col: number): boolean => {
const moduleRow = row - QUIET_ZONE_MODULES;
const moduleCol = col - QUIET_ZONE_MODULES;
if (moduleRow < 0 || moduleCol < 0 || moduleRow >= moduleCount || moduleCol >= moduleCount) {
return false;
}
return qr.isDark(moduleRow, moduleCol);
},
};
}
export function renderQrLines(text: string): string[] {
const { size, isDark } = qrGrid(text);
const lines: string[] = [];
for (let row = 0; row < size; row += 2) {
let line = "";
for (let col = 0; col < size; col += 1) {
const top = isDark(row, col);
// An odd total height leaves the last line with no bottom row; pad it light.
const bottom = row + 1 < size && isDark(row + 1, col);
line += top ? (bottom ? BOTH_DARK : TOP_DARK) : (bottom ? BOTTOM_DARK : BOTH_LIGHT);
}
lines.push(line);
}
return lines;
}
/**
* One `background-image` value for the desktop host. Runs of dark modules are
* merged into a single rect per run so the markup stays small, and
* `shape-rendering="crispEdges"` keeps module edges from being blurred away.
*/
export function renderQrSvgDataUri(text: string): string {
const { size, isDark } = qrGrid(text);
let rects = "";
for (let row = 0; row < size; row += 1) {
let runStart: number | null = null;
for (let col = 0; col <= size; col += 1) {
const dark = col < size && isDark(row, col);
if (dark && runStart === null) runStart = col;
if (!dark && runStart !== null) {
rects += ``;
runStart = null;
}
}
}
const svg = `";
return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
}