Skip to content

Commit

Permalink
Clear the QR code underneath the logo
Browse files Browse the repository at this point in the history
  • Loading branch information
Kostiantyn Ko committed Sep 1, 2024
1 parent 70056ea commit 66a127d
Show file tree
Hide file tree
Showing 20 changed files with 129 additions and 75 deletions.
21 changes: 21 additions & 0 deletions src/matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,24 @@ export function getMatrix(data: Data) {

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

export function clearMatrixCenter(matrix: Matrix, widthPct: number, heightPct: number): Matrix {
matrix = matrix.map((x) => x.slice()); // avoid mutating input arg

// TODO: Here's a homegrown formula, perhaps could be simplified
const mW = matrix.length;
const cW = Math.ceil(((mW * widthPct) / 100 + (mW % 2)) / 2) * 2 - (mW % 2);
const mH = matrix[0]?.length ?? 0;
const cH = Math.ceil(((mH * heightPct) / 100 + (mH % 2)) / 2) * 2 - (mH % 2);

// Given the formula, these must be whole numbers, but round anyway to account for js EPSILON
const clearStartX = Math.round((mW - cW) / 2);
const clearStartY = Math.round((mH - cH) / 2);

for (let x = clearStartX; x < clearStartX + cW; x += 1) {
for (let y = clearStartY; y < clearStartY + cH; y += 1) {
matrix[x][y] = 0;
}
}
return matrix;
}
40 changes: 23 additions & 17 deletions src/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import { QR } from "./qr-base.js";
import { ImageOptions, Matrix } from "./typing/types";
import { getOptions, getSVGPath } from "./utils.js";
import colorString from "color-string";
import { clearMatrixCenter } from "./matrix.js";

const textDec = new TextDecoder();

export async function getPDF(text: string, inOptions: ImageOptions) {
const options = getOptions(inOptions);
const matrix = QR(text, options.ec_level, options.parse_url);

let matrix = QR(text, options.ec_level, options.parse_url);
if (options.logo && options.logoWidth && options.logoHeight) {
matrix = clearMatrixCenter(matrix, options.logoWidth, options.logoHeight);
}

return PDF({ matrix, ...options });
}

Expand All @@ -17,18 +23,14 @@ function colorToRGB(color: string | number): [number, number, number] {
const [red, green, blue] = colorString.get.rgb(color);
return [red / 255, green / 255, blue / 255];
}
return [
((color >>> 24) % 256) / 255,
((color >>> 16) % 256) / 255,
((color >>> 8) % 256) / 255,
];
return [((color >>> 24) % 256) / 255, ((color >>> 16) % 256) / 255, ((color >>> 8) % 256) / 255];
}

function getOpacity(color: string | number): number {
if (typeof color === "string") {
return colorString.get.rgb(color)[3];
}
return ((color % 256) / 255);
return (color % 256) / 255;
}

async function PDF({
Expand All @@ -44,15 +46,19 @@ async function PDF({
matrix: Matrix;
}) {
const size = 9;
const marginPx = margin * size;
const matrixSizePx = matrix.length * size;
const imageSizePx = matrixSizePx + 2 * marginPx;

const document = await PDFDocument.create();
const pageSize = (matrix.length + 2 * margin) * size;
const page = document.addPage([pageSize, pageSize]);
const page = document.addPage([imageSizePx, imageSizePx]);
page.drawSquare({
size: pageSize,
size: imageSizePx,
color: rgb(...colorToRGB(bgColor)),
});
page.moveTo(0, page.getHeight());
const path = getSVGPath(matrix, size, margin * size, borderRadius);

const path = getSVGPath(matrix, size, marginPx, borderRadius);
page.drawSvgPath(path, {
color: rgb(...colorToRGB(color)),
opacity: getOpacity(color),
Expand All @@ -67,13 +73,13 @@ async function PDF({
} else {
logoData = await document.embedJpg(logo);
}
const logoWidthPx = (logoWidth / 100) * matrixSizePx;
const logoHeightPx = (logoHeight / 100) * matrixSizePx;
page.drawImage(logoData, {
x: page.getWidth() / 2 - (logoWidth / 100) * (page.getWidth() / 2),
y:
page.getHeight() / 2 -
(logoHeight / 100) * (page.getWidth() / 2),
width: (logoWidth / 100) * page.getWidth(),
height: (logoHeight / 100) * page.getHeight(),
x: (imageSizePx - logoWidthPx) / 2,
y: (imageSizePx - logoHeightPx) / 2,
width: logoWidthPx,
height: logoHeightPx,
});
}
return document.save();
Expand Down
25 changes: 18 additions & 7 deletions src/png.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ 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";

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

let matrix = QR(text, options.ec_level, options.parse_url);
if (options.logo && options.logoWidth && options.logoHeight) {
matrix = clearMatrixCenter(matrix, options.logoWidth, options.logoHeight);
}

return generateImage({ matrix, ...options, type: "png" });
}

Expand All @@ -22,26 +28,31 @@ export async function generateImage({
borderRadius,
}: ImageOptions & { matrix: Matrix }) {
const marginPx = margin * size;
const imageSize = matrix.length * size + marginPx * 2;
const matrixSizePx = matrix.length * size;
const imageSizePx = matrixSizePx + marginPx * 2;

if (size > 200) {
throw new Error("Module size is too big, resulting image is too large: " + imageSize);
throw new Error("Module size is too big, resulting image is too large: " + imageSizePx);
}

const svg = await createSVG({
matrix,
size,
margin,
color,
bgColor,
imageWidth: imageSize,
imageHeight: imageSize,
imageWidth: imageSizePx,
imageHeight: imageSizePx,
logoWidth: logo && logoWidth,
logoHeight: logo && logoHeight,
borderRadius,
});
const qrImage = sharp(svg);
const layers: sharp.OverlayOptions[] = [];
if (logo) {
const sharpLogo = sharp(logo).resize(
Math.round((imageSize * logoWidth) / 100),
Math.round((imageSize * logoHeight) / 100),
Math.round((matrixSizePx * logoWidth) / 100),
Math.round((matrixSizePx * logoHeight) / 100),
{ fit: "contain" }
);
const data = await sharpLogo.toBuffer();
Expand Down
45 changes: 28 additions & 17 deletions src/png_browser.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import { QR } from "./qr-base.js";
import { getOptions, colorToHex, getSVGPath } from "./utils.js";
import { colorToHex, getOptions, getSVGPath } from "./utils.js";
import { ImageOptions, Matrix } from "./typing/types";
import { Base64 } from "js-base64";
import { clearMatrixCenter } from "./matrix.js";

export async function getPNG(text: string, inOptions: ImageOptions) {
const options = getOptions(inOptions);
const matrix = QR(text, options.ec_level, options.parse_url);
return generateImage({ matrix, ...options, type: 'png' });

let matrix = QR(text, options.ec_level, options.parse_url);
if (options.logo && options.logoWidth && options.logoHeight) {
matrix = clearMatrixCenter(matrix, options.logoWidth, options.logoHeight);
}

return generateImage({ matrix, ...options, type: "png" });
}

function dataURItoArrayBuffer(dataURI: string) {
return Base64.toUint8Array(dataURI.split(',')[1]);
return Base64.toUint8Array(dataURI.split(",")[1]);
}

function blobToDataURL(blob: Blob) {
return new Promise<string>((resolve, reject) => {
try {
var a = new FileReader();
a.onload = function(e) {resolve(e.target.result as string);}
a.onload = function (e) {
resolve(e.target.result as string);
};
a.onerror = reject;
a.readAsDataURL(blob);
} catch (e) {
Expand All @@ -26,7 +34,6 @@ function blobToDataURL(blob: Blob) {
});
}


export async function generateImage({
matrix,
size,
Expand All @@ -39,14 +46,16 @@ export async function generateImage({
borderRadius,
}: ImageOptions & { matrix: Matrix }) {
const marginPx = margin * size;
const imageSize = matrix.length * size + marginPx * 2;
const matrixSizePx = matrix.length * size;
const imageSizePx = matrixSizePx + marginPx * 2;

const canvas = document.createElement('canvas');
canvas.width = imageSize;
canvas.height = imageSize;
const context = canvas.getContext('2d');
const canvas = document.createElement("canvas");
canvas.width = imageSizePx;
canvas.height = imageSizePx;
const context = canvas.getContext("2d");
context.fillStyle = colorToHex(bgColor);
context.fillRect(0, 0, imageSize, imageSize);
context.fillRect(0, 0, imageSizePx, imageSizePx);

const path = new Path2D(getSVGPath(matrix, size, marginPx, borderRadius));
context.fillStyle = colorToHex(color);
context.fill(path);
Expand All @@ -61,13 +70,15 @@ export async function generateImage({
reject(e);
}
});
const logoWidthPx = (logoWidth / 100) * matrixSizePx;
const logoHeightPx = (logoHeight / 100) * matrixSizePx;
context.drawImage(
logoImage,
imageSize / 2 - (logoWidth / 2 / 100) * imageSize,
imageSize / 2 - (logoHeight / 2 / 100) * imageSize,
(logoWidth / 100) * imageSize,
(logoHeight / 100) * imageSize
(imageSizePx - logoWidthPx) / 2,
(imageSizePx - logoHeightPx) / 2,
logoWidthPx,
logoHeightPx
);
}
return dataURItoArrayBuffer(canvas.toDataURL('image/png'));
return dataURItoArrayBuffer(canvas.toDataURL("image/png"));
}
59 changes: 32 additions & 27 deletions src/svg.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { clearMatrixCenter } from "./matrix.js";
import { QR } from "./qr-base.js";
import { ImageOptions, Matrix } from "./typing/types";
import { getOptions, colorToHex, getSVGPath } from "./utils.js";
import { colorToHex, getOptions, getSVGPath } from "./utils.js";
import { Base64 } from "js-base64";

interface FillSVGOptions
extends Pick<
ImageOptions,
"color" | "bgColor" | "size" | "margin" | "borderRadius"
> {
interface FillSVGOptions extends Pick<ImageOptions, "color" | "bgColor" | "size" | "margin" | "borderRadius"> {
blockSize?: number;
}

export async function getSVG(text: string, inOptions: ImageOptions = {}) {
const options = getOptions({ ...inOptions, type: "svg" });
const matrix = QR(text, options.ec_level, options.parse_url);

let matrix = QR(text, options.ec_level, options.parse_url);
if (options.logo && options.logoWidth && options.logoHeight) {
matrix = clearMatrixCenter(matrix, options.logoWidth, options.logoHeight);
}

return createSVG({ matrix, ...options });
}

Expand All @@ -36,53 +38,56 @@ export async function createSVG({
imageWidth?: number;
imageHeight?: number;
}): Promise<Uint8Array> {
const actualSize = size || 9;
const X = matrix.length + 2 * margin;
const XY = X * (actualSize || 1);
const actualBlockSize = size || 9;
const matrixSizePx = matrix.length * actualBlockSize;
const marginPx = margin * actualBlockSize;
const imageSizePx = matrixSizePx + 2 * marginPx;
const imageWidthStr = imageWidth ? ` width="${imageWidth}"` : "";
const imageHeightStr = imageHeight ? `height="${imageWidth}" ` : "";
const imageHeightStr = imageHeight ? ` height="${imageWidth}"` : "";
const xmlTag = `<?xml version="1.0" encoding="utf-8"?>`;
const svgOpeningTag = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink"${imageWidthStr} ${imageHeightStr}viewBox="0 0 ${XY} ${XY}">`;
const svgOpeningTag = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink"${imageWidthStr}${imageHeightStr} viewBox="0 0 ${imageSizePx} ${imageSizePx}">`;

const svgBody = getSVGBody(matrix, {
color,
bgColor,
size: XY,
size: imageSizePx,
margin,
blockSize: actualSize,
blockSize: actualBlockSize,
borderRadius,
});
const svgEndTag = "</svg>";
const logoImage = logo ? getLogoImage(logo, XY, logoWidth, logoHeight) : "";
const logoImage = logo ? getLogoImage(logo, marginPx, matrixSizePx, logoWidth, logoHeight) : "";

return te.encode(
xmlTag + svgOpeningTag + svgBody + logoImage + svgEndTag
);
return te.encode(xmlTag + svgOpeningTag + svgBody + logoImage + svgEndTag);
}

function getSVGBody(matrix: Matrix, options: FillSVGOptions): string {
const path = getSVGPath(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) + '"/>';
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)}"/>`;
return svgBody;
}

function getLogoImage(
logo: ImageOptions["logo"],
XY: number,
marginPx: number,
matrixSizePx: number,
logoWidth: ImageOptions["logoWidth"],
logoHeight: ImageOptions["logoHeight"]
): string {
const imageBase64 = `data:image/png;base64,${Base64.fromUint8Array(new Uint8Array(logo))}`;
const logoWidthPx = (logoWidth / 100) * matrixSizePx;
const logoHeightPx = (logoHeight / 100) * matrixSizePx;

return (
`<image ` +
`width="${(logoWidth / 100) * XY}" ` +
`height="${(logoHeight / 100) * XY}" ` +
`width="${logoWidthPx}" ` +
`height="${logoHeightPx}" ` +
`xlink:href="${imageBase64}" ` +
`x="${XY / 2 - ((logoWidth / 100) * XY) / 2}" ` +
`y="${XY / 2 - ((logoHeight / 100) * XY) / 2}">` +
`x="${marginPx + (matrixSizePx - logoWidthPx) / 2}" ` +
`y="${marginPx + (matrixSizePx - logoHeightPx) / 2}">` +
`</image>`
);
}
Binary file modified test_data/golden/browser_qr_logo_arraybuffer.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion test_data/golden/browser_qr_with_border_radius.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test_data/golden/browser_qr_with_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion test_data/golden/browser_qr_with_logo_as_arraybuffer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test_data/golden/browser_qr_with_logo_jpg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test_data/golden/qr_logo_arraybuffer.pdf
Binary file not shown.
Binary file modified test_data/golden/qr_logo_arraybuffer_jpg.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion test_data/golden/qr_with_border_radius.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test_data/golden/qr_with_logo.pdf
Binary file not shown.
Binary file modified test_data/golden/qr_with_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion test_data/golden/qr_with_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion test_data/golden/qr_with_logo_as_arraybuffer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion test_data/golden/qr_with_logo_as_arraybuffer_jpg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test_data/golden/qr_with_logo_jpg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 66a127d

Please sign in to comment.