diff --git a/src/matrix.ts b/src/matrix.ts index 96e0bad..47d2b66 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -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; @@ -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); @@ -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 diff --git a/src/pdf.ts b/src/pdf.ts index 5bbc0e0..332dc4a 100644 --- a/src/pdf.ts +++ b/src/pdf.ts @@ -1,9 +1,9 @@ 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(); @@ -11,6 +11,7 @@ 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); } @@ -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), diff --git a/src/png.ts b/src/png.ts index b244d8e..8ad383d 100644 --- a/src/png.ts +++ b/src/png.ts @@ -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); } diff --git a/src/png_browser.ts b/src/png_browser.ts index 691d007..1e89ad5 100644 --- a/src/png_browser.ts +++ b/src/png_browser.ts @@ -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); } @@ -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) { diff --git a/src/qr-base.ts b/src/qr-base.ts index 53c73a5..edf69ef 100644 --- a/src/qr-base.ts +++ b/src/qr-base.ts @@ -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) { diff --git a/src/svg.ts b/src/svg.ts index e248b4a..3469afc 100644 --- a/src/svg.ts +++ b/src/svg.ts @@ -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 { @@ -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); } @@ -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 = ``; - svgBody += ``; + svgBody += ``; + svgBody += ``; return svgBody; } diff --git a/src/typing/types.ts b/src/typing/types.ts index 3f11a2a..07ff3e1 100644 --- a/src/typing/types.ts +++ b/src/typing/types.ts @@ -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[][]; diff --git a/src/utils.ts b/src/utils.ts index 6c83607..24b56bf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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];