Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clear the QR code matrix underneath the logo, fix #15 #16

Merged
merged 2 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"tabWidth": 4,
"useTabs": false,
"singleQuote": false
"singleQuote": false,
"printWidth": 120
}
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
48 changes: 34 additions & 14 deletions src/png.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { ImageOptions, Matrix } from "./typing/types";
import { QR } from "./qr-base.js";
import { createSVG } from './svg.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);
return generateImage({ matrix, ...options, type: 'png' });
const options = getOptions({ ...inOptions, 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" });
}

export async function generateImage({
Expand All @@ -22,27 +28,41 @@ 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,
matrix,
size,
margin,
color,
bgColor,
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), {fit: 'contain'});
const data = await sharpLogo.toBuffer()
const sharpLogo = sharp(logo).resize(
Math.round((matrixSizePx * logoWidth) / 100),
Math.round((matrixSizePx * logoHeight) / 100),
{ fit: "contain" }
);
const data = await sharpLogo.toBuffer();
layers.push({
input: data,
})
});
qrImage.composite(layers);
}
const { data } = await qrImage.png({
palette: !logo, // no logo results in much less colors
}).toBuffer({ resolveWithObject: true});
const { data } = await qrImage
.png({ palette: !logo }) // no logo results in much less colors
.toBuffer({ resolveWithObject: true });
return new Uint8ClampedArray(data.buffer);
}
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"));
}
60 changes: 34 additions & 26 deletions src/svg.ts
Original file line number Diff line number Diff line change
@@ -1,16 +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 { Base64 } from 'js-base64';
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);
const options = getOptions({ ...inOptions, type: "svg" });

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 @@ -33,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>`
);
}
Loading