Skip to content

Commit

Permalink
feat(qr): draw finders using a different SVG path, allows custom styl…
Browse files Browse the repository at this point in the history
…es for finders
  • Loading branch information
gugu committed Sep 30, 2024
1 parent 9f4c31d commit ca0143f
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 30 deletions.
57 changes: 41 additions & 16 deletions src/matrix.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,65 @@
import { Data, EcLevel, Matrix } from "./typing/types";
import { BitMatrix, Data, EcLevel, Matrix } from "./typing/types";

// {{{1 Initialize matrix with zeros
export function init(version: number): Matrix {
const N = (version << 2) + 0b10001;
const matrix: Matrix = [];
let zeros: number[] = Array(N).fill(0);
for (let i = 0; i < N; i++) {
matrix[i] = [...zeros];
}
return matrix;
return Array(N).fill(null).map(() => Array(N).fill(0))
}

// {{{1 Put finders into matrix
// Not used
export function fillFinders(matrix: Matrix) {
const N = matrix.length;
for (var i = -3; i <= 3; i++) {
for (let i = -3; i <= 3; i++) {
for (let j = -3; j <= 3; j++) {
const max = Math.max(i, j);
const min = Math.min(i, j);
const pixel =
(max == 2 && min >= -2) || (min == -2 && max <= 2)
? 0x80
: 0x81;
? 0b010_000_000
: 0b010_000_001;
matrix[3 + i][3 + j] = pixel;
matrix[3 + i][N - 4 + j] = pixel;
matrix[N - 4 + i][3 + j] = pixel;
}
}
for (var i = 0; i < 8; i++) {
for (let i = 0; i < 8; i++) {
matrix[7][i] =
matrix[i][7] =
matrix[7][N - i - 1] =
matrix[i][N - 8] =
matrix[N - 8][i] =
matrix[N - 1 - i][7] =
0x80;
0b010_000_000;
}
}

/**
* Finders require different UI representation, so we zero-fill finders and draw them later
*/
export function zeroFillFinders(matrix: BitMatrix) {
const N = matrix.length;
const zeroPixel = 0;
// squares
for (let i = -3; i <= 3; i++) {
for (let j = -3; j <= 3; j++) {
matrix[3 + i][3 + j] = zeroPixel;
matrix[3 + i][N - 4 + j] = zeroPixel;
matrix[N - 4 + i][3 + j] = zeroPixel;
}
}
// border
for (let i = 0; i < 8; i++) {
matrix[7][i] =
matrix[i][7] =
matrix[7][N - i - 1] =
matrix[i][N - 8] =
matrix[N - 8][i] =
matrix[N - 1 - i][7] =
zeroPixel;
}
}


// {{{1 Put align and timinig
export function fillAlignAndTiming(matrix: Matrix) {
const N = matrix.length;
Expand Down Expand Up @@ -368,7 +390,7 @@ export function calculatePenalty(matrix: Matrix) {
}

// {{{1 All-in-one function
export function getMatrix(data: Data) {
export function getMatrix(data: Data): BitMatrix {
const matrix = init(data.version);
fillFinders(matrix);
fillAlignAndTiming(matrix);
Expand All @@ -389,10 +411,13 @@ export function getMatrix(data: Data) {
fillData(matrix, data, bestMask);
fillReserved(matrix, data.ec_level, bestMask);

return matrix.map((row) => row.map((cell) => cell & 1));
return matrix.map((row) => row.map((cell) => (cell & 1) as 0 | 1));
}

export function clearMatrixCenter(matrix: Matrix, widthPct: number, heightPct: number): Matrix {
/**
* Before we insert logo in the QR we need to clear pixels under the logo. This function clears pixels
*/
export function clearMatrixCenter(matrix: BitMatrix, widthPct: number, heightPct: number): BitMatrix {
matrix = matrix.map((x) => x.slice()); // avoid mutating input arg

// TODO: Here's a homegrown formula, perhaps could be simplified
Expand Down
7 changes: 4 additions & 3 deletions src/pdf.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { PDFDocument, PDFImage, rgb } from "pdf-lib";
import { QR } from "./qr-base.js";
import { ImageOptions, Matrix } from "./typing/types";
import { getOptions, getSVGPath } from "./utils.js";
import { getOptions, getDotsSVGPath } from "./utils.js";
import colorString from "color-string";
import { clearMatrixCenter } from "./matrix.js";
import { clearMatrixCenter, zeroFillFinders } from "./matrix.js";

const textDec = new TextDecoder();

export async function getPDF(text: string, inOptions: ImageOptions) {
const options = getOptions(inOptions);

let matrix = QR(text, options.ec_level, options.parse_url);
zeroFillFinders(matrix)
if (options.logo && options.logoWidth && options.logoHeight) {
matrix = clearMatrixCenter(matrix, options.logoWidth, options.logoHeight);
}
Expand Down Expand Up @@ -58,7 +59,7 @@ async function PDF({
});
page.moveTo(0, page.getHeight());

const path = getSVGPath(matrix, size, marginPx, borderRadius);
const path = getDotsSVGPath(matrix, size, marginPx, borderRadius);
page.drawSvgPath(path, {
color: rgb(...colorToRGB(color)),
opacity: getOpacity(color),
Expand Down
3 changes: 2 additions & 1 deletion src/png.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { QR } from "./qr-base.js";
import { createSVG } from "./svg.js";
import { getOptions } from "./utils.js";
import sharp from "sharp";
import { clearMatrixCenter } from "./matrix.js";
import { clearMatrixCenter, zeroFillFinders } from "./matrix.js";

export async function getPNG(text: string, inOptions: ImageOptions = {}) {
const options = getOptions({ ...inOptions, type: "png" });

let matrix = QR(text, options.ec_level, options.parse_url);
zeroFillFinders(matrix)
if (options.logo && options.logoWidth && options.logoHeight) {
matrix = clearMatrixCenter(matrix, options.logoWidth, options.logoHeight);
}
Expand Down
7 changes: 4 additions & 3 deletions src/png_browser.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { QR } from "./qr-base.js";
import { colorToHex, getOptions, getSVGPath } from "./utils.js";
import { colorToHex, getOptions, getDotsSVGPath } from "./utils.js";
import { ImageOptions, Matrix } from "./typing/types";
import { Base64 } from "js-base64";
import { clearMatrixCenter } from "./matrix.js";
import { clearMatrixCenter, zeroFillFinders } from "./matrix.js";

export async function getPNG(text: string, inOptions: ImageOptions) {
const options = getOptions(inOptions);

let matrix = QR(text, options.ec_level, options.parse_url);
zeroFillFinders(matrix)
if (options.logo && options.logoWidth && options.logoHeight) {
matrix = clearMatrixCenter(matrix, options.logoWidth, options.logoHeight);
}
Expand Down Expand Up @@ -56,7 +57,7 @@ export async function generateImage({
context.fillStyle = colorToHex(bgColor);
context.fillRect(0, 0, imageSizePx, imageSizePx);

const path = new Path2D(getSVGPath(matrix, size, marginPx, borderRadius));
const path = new Path2D(getDotsSVGPath(matrix, size, marginPx, borderRadius));
context.fillStyle = colorToHex(color);
context.fill(path);
if (logo) {
Expand Down
2 changes: 1 addition & 1 deletion src/qr-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function getTemplate(message: NumberData, ec_level: EcLevel): Data {

// {{{1 Fill template
export function fillTemplate(message: NumberData, template: Data): Data {
const blocks = new Uint8Array(template.data_len);
const blocks = new Uint8ClampedArray(template.data_len);
let messageUpdated: number[];

if (template.version < 10) {
Expand Down
11 changes: 7 additions & 4 deletions src/svg.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { clearMatrixCenter } from "./matrix.js";
import { clearMatrixCenter, zeroFillFinders } from "./matrix.js";
import { QR } from "./qr-base.js";
import { ImageOptions, Matrix } from "./typing/types";
import { colorToHex, getOptions, getSVGPath } from "./utils.js";
import { colorToHex, getOptions, getDotsSVGPath, getFindersSVGPath } from "./utils.js";
import { Base64 } from "js-base64";

interface FillSVGOptions extends Pick<ImageOptions, "color" | "bgColor" | "size" | "margin" | "borderRadius"> {
Expand All @@ -12,6 +12,7 @@ export async function getSVG(text: string, inOptions: ImageOptions = {}) {
const options = getOptions({ ...inOptions, type: "svg" });

let matrix = QR(text, options.ec_level, options.parse_url);
zeroFillFinders(matrix)
if (options.logo && options.logoWidth && options.logoHeight) {
matrix = clearMatrixCenter(matrix, options.logoWidth, options.logoHeight);
}
Expand Down Expand Up @@ -62,11 +63,13 @@ export async function createSVG({
}

function getSVGBody(matrix: Matrix, options: FillSVGOptions): string {
const path = getSVGPath(matrix, options.blockSize, options.margin * options.blockSize, options.borderRadius);
const dotsPath = getDotsSVGPath(matrix, options.blockSize, options.margin * options.blockSize, options.borderRadius);
const outerFindersPath = getFindersSVGPath(matrix, options.blockSize, options.margin * options.blockSize, options.borderRadius);
let svgBody = `<rect width="${options.size}" height="${options.size}" fill="${colorToHex(
options.bgColor
)}"></rect>`;
svgBody += `<path shape-rendering="geometricPrecision" d="${path}" fill="${colorToHex(options.color)}"/>`;
svgBody += `<path shape-rendering="geometricPrecision" d="${outerFindersPath}" fill-rule="evenodd" fill="${colorToHex(options.color)}"/>`;
svgBody += `<path shape-rendering="geometricPrecision" d="${dotsPath}" fill="${colorToHex(options.color)}"/>`;
return svgBody;
}

Expand Down
4 changes: 3 additions & 1 deletion src/typing/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export type Matrix = number[][];
export type MatrixValue = 0 | 1 | 128 | 129;
export type Matrix = MatrixValue[][];
export type BitMatrix = (0 | 1)[][];

export interface Data {
blocks: number[][];
Expand Down
30 changes: 29 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,35 @@ export function colorToHex(color: number | string): string {
return `#${(color >>> 8).toString(16).padStart(6, "0")}`;
}

export function getSVGPath(matrix: Matrix, size: number, margin: number = 0, borderRadius: number = 0) {

export function getFindersSVGPath(matrix: Matrix, size: number = 0, margin: number = 0, borderRadius: number = 0) {
const matrixSize = matrix.length * size + margin * 2;
let finderSize = 8;
let finderEnd = finderSize - 1;
const sides = [[0, 0], [1, 0], [0, 1]]
const rectangles = [];
for (const side of sides) {
const signs = side.map(sidePoint => sidePoint == 0 ? 1 : -1);
for (const offset of [0, 1, 2]) {
let corners = [
[matrixSize * side[0] + signs[0] * (margin + size * offset), matrixSize * side[1] + signs[1] * (margin + size * offset)],
[matrixSize * side[0] + signs[0] * (margin + size * (finderEnd - offset)), matrixSize * side[1] + signs[1] * (margin + size * (finderEnd - offset))],
]
let rectangle = [
'M', corners[0][0], corners[0][1],
'L', corners[0][0], corners[1][1],
'L', corners[1][0], corners[1][1],
'L', corners[1][0], corners[0][1],
'z',
]
rectangles.push(...rectangle)
}
}

return rectangles.join(" ")
}

export function getDotsSVGPath(matrix: Matrix, size: number, margin: number = 0, borderRadius: number = 0) {
let rectangles = [];
for (let x = 0; x < matrix.length; x++) {
const column = matrix[x];
Expand Down

0 comments on commit ca0143f

Please sign in to comment.