Skip to content

Commit

Permalink
svg path support, border radius support
Browse files Browse the repository at this point in the history
  • Loading branch information
gugu committed Sep 11, 2023
1 parent 7ddbb7d commit 6f8b368
Show file tree
Hide file tree
Showing 49 changed files with 132 additions and 56 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,4 @@ getSVG with logo x 2,817 ops/sec ±0.17% (89 runs sampled)

- Use lighter versions of PDF library
- Round corners
- Transparency
- Background
39 changes: 19 additions & 20 deletions src/pdf.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PDFDocument, PDFImage, rgb } from "pdf-lib";
import { QR } from "./qr-base.js";
import { ImageOptions, Matrix } from "./typing/types";
import { getOptions } from "./utils.js";
import { getOptions, getSVGPath } from "./utils.js";
import colorString from "color-string";

const textDec = new TextDecoder();
Expand All @@ -24,6 +24,13 @@ function colorToRGB(color: string | number): [number, number, number] {
];
}

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

async function PDF({
matrix,
margin,
Expand All @@ -32,34 +39,26 @@ async function PDF({
logoHeight,
color,
bgColor,
borderRadius,
}: ImageOptions & {
matrix: Matrix;
}) {
const size = 9;
const document = await PDFDocument.create();
const pageSize = (matrix.length + 2 * margin) * size;
const page = document.addPage([pageSize, pageSize]);
page.drawRectangle({
width: pageSize,
height: pageSize,
page.drawSquare({
size: pageSize,
color: rgb(...colorToRGB(bgColor)),
});
page.moveTo(margin * size, page.getHeight() - margin * size - size);
for (const column of matrix) {
for (const y of column) {
if (y) {
page.drawRectangle({
width: size,
height: size,
color: rgb(...colorToRGB(color)),
borderColor: rgb(...colorToRGB(color)),
});
}
page.moveDown(size);
}
page.moveUp(size * column.length);
page.moveRight(size);
}
page.moveTo(0, page.getHeight());
const path = getSVGPath(matrix, size, margin * size, borderRadius);
page.drawSvgPath(path, {
color: rgb(...colorToRGB(color)),
opacity: getOpacity(color),
borderColor: rgb(...colorToRGB(color)),
borderOpacity: getOpacity(color),
});
if (logo) {
let logoData: PDFImage;
const header = new Uint8Array(logo.slice(0, 4));
Expand Down
2 changes: 2 additions & 0 deletions src/png.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ export async function generateImage({
logoHeight,
color,
bgColor,
borderRadius,
}: ImageOptions & { matrix: Matrix }) {
const marginPx = margin * size;
const imageSize = matrix.length * size + marginPx * 2;
const svg = await createSVG({
matrix, size, margin, color, bgColor,
imageWidth: imageSize, imageHeight: imageSize,
borderRadius,
});
const qrImage = sharp(svg);
const layers: sharp.OverlayOptions[] = [];
Expand Down
2 changes: 1 addition & 1 deletion src/png_browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function generateImage({
const context = canvas.getContext('2d');
context.fillStyle = colorToHex(bgColor);
context.fillRect(0, 0, imageSize, imageSize);

// TODO use Path2D when node-canvas supports it, currently it makes testing impossible
for (let y = 0; y < N; y += 1) {
for (let x = 0; x < matrix[y].length; x += 1) {
if (matrix[y][x]) {
Expand Down
25 changes: 8 additions & 17 deletions src/svg.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { QR } from "./qr-base.js";
import { ImageOptions, Matrix } from "./typing/types";
import { getOptions, colorToHex } from "./utils.js";
import { getOptions, colorToHex, getSVGPath } from "./utils.js";
import { Base64 } from 'js-base64';

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

Expand All @@ -27,6 +27,7 @@ export async function createSVG({
bgColor,
imageWidth,
imageHeight,
borderRadius,
}: ImageOptions & {
matrix: Matrix;
imageWidth?: number;
Expand All @@ -45,6 +46,7 @@ export async function createSVG({
size: XY,
margin,
blockSize: actualSize,
borderRadius,
});
const svgEndTag = "</svg>";
const logoImage = logo ? getLogoImage(logo, XY, logoWidth, logoHeight) : "";
Expand All @@ -55,22 +57,11 @@ export async function createSVG({
}

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>`;

for (let y = 0; y < matrix.length; y++) {
for (let x = 0; x < matrix[y].length; x++) {
if (matrix[y][x]) {
svgBody +=
`<rect shape-rendering="geometricPrecision" width="${options.blockSize}" height="${options.blockSize}" ` +
`fill="${colorToHex(options.color)}" ` +
`x="${(x + options.margin) * options.blockSize}" ` +
`y="${(y + options.margin) * options.blockSize}">` +
`</rect>`;
}
}
}
`<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;
}

Expand Down
24 changes: 24 additions & 0 deletions src/tests/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ const defaultParams = {
filename: "qr.png",

},
{
name: "PNG with border radius",
type: "png",
filename: "qr_with_border_radius.png",
params: {
borderRadius: 2,
}
},
{
name: "PNG with colors",
type: "png",
Expand Down Expand Up @@ -91,6 +99,14 @@ const defaultParams = {
type: "svg",
filename: "qr.svg",
},
{
name: "SVG with border radius",
type: "svg",
filename: "qr_with_border_radius.svg",
params: {
borderRadius: 2,
}
},
{
name: "SVG with EC level",
type: "svg",
Expand Down Expand Up @@ -146,6 +162,14 @@ const defaultParams = {
type: "pdf",
filename: "qr.pdf",
},
{
name: "PDF with border radius",
type: "pdf",
filename: "qr_with_border_radius.pdf",
params: {
borderRadius: 2,
}
},
{
name: "PDF with colors",
type: "pdf",
Expand Down
24 changes: 24 additions & 0 deletions src/tests/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ interface TestParams {
margin: 3,
}
},
{
name: "PNG with border radius",
fn: getPNG,
filename: "qr_with_border_radius.png",
params: {
borderRadius: 1,
},
},
{
name: "PNG with colors",
fn: getPNG,
Expand Down Expand Up @@ -110,6 +118,14 @@ interface TestParams {
fn: getSVG,
filename: "qr.svg",
},
{
name: "SVG with border radius",
fn: getSVG,
filename: "qr_with_border_radius.svg",
params: {
borderRadius: 4,
},
},
{
name: "SVG with EC level",
fn: getSVG,
Expand Down Expand Up @@ -180,6 +196,14 @@ interface TestParams {
fn: getPDF,
filename: "qr.pdf",
},
{
name: "PDF with border radius",
fn: getPDF,
filename: "qr_with_border_radius.pdf",
params: {
borderRadius: 4,
}
},
{
name: "PDF with colors",
fn: getPDF,
Expand Down
5 changes: 5 additions & 0 deletions src/typing/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,9 @@ export interface ImageOptions {
* @default 0xFFFFFFFF
*/
bgColor?: number | string;

/**
* border radius of the points
*/
borderRadius?: number;
}
32 changes: 31 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import colorString from "color-string";
import { ImageOptions, ImageType } from "./typing/types";
import { ImageOptions, ImageType, Matrix } from "./typing/types";

export function getOptions(inOptions: ImageOptions) {
const type: ImageType = inOptions?.type ?? "png";
Expand All @@ -14,6 +14,36 @@ 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) {
let rectangles = [];
for (let x = 0; x < matrix.length; x++) {
const column = matrix[x];
for (let y = 0; y < column.length; y++) {
if (column[y]) {
const leftX = x * size + margin;
const rightX = (x + 1) * size + margin;
const topY = y * size + margin;
const bottomY = (y + 1) * size + margin;
const rectangle = [];
rectangle.push(`M ${leftX} ${topY + borderRadius}`)
rectangle.push(`L ${leftX} ${bottomY - borderRadius}`)
rectangle.push(`A ${borderRadius} ${borderRadius} 0 0 0 ${leftX + borderRadius} ${bottomY} `)
rectangle.push(`L ${rightX - borderRadius} ${bottomY}`)
rectangle.push(`A ${borderRadius} ${borderRadius} 0 0 0 ${rightX} ${bottomY - borderRadius}`)
rectangle.push(`L ${rightX} ${topY + borderRadius}`)
rectangle.push(`A ${borderRadius} ${borderRadius} 0 0 0 ${rightX - borderRadius} ${topY}`)
rectangle.push(`L ${leftX + borderRadius} ${topY}`)
rectangle.push(`A ${borderRadius} ${borderRadius} 0 0 0 ${leftX} ${topY + borderRadius}`)
rectangle.push(`z`)
rectangles.push(rectangle.join(" "));
}
}
}
return rectangles.join(" ");
}



const commonOptions: Pick<
ImageOptions,
| "type"
Expand Down
Binary file modified test_data/golden/browser_qr.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion test_data/golden/browser_qr.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_logo_arraybuffer.pdf
Binary file not shown.
Binary file not shown.
Binary file added test_data/golden/browser_qr_with_border_radius.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions 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_colors.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion test_data/golden/browser_qr_with_colors.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/browser_qr_with_colors_hex.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/browser_qr_with_ec_level.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.pdf
Binary file not shown.
2 changes: 1 addition & 1 deletion test_data/golden/browser_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/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.
2 changes: 1 addition & 1 deletion test_data/golden/browser_qr_with_size.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.pdf
Binary file not shown.
Binary file modified test_data/golden/qr.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.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 added test_data/golden/qr_logo_arraybuffer.pdf
Binary file not shown.
Binary file added test_data/golden/qr_logo_arraybuffer_jpg.pdf
Binary file not shown.
Binary file added test_data/golden/qr_with_border_radius.pdf
Binary file not shown.
Binary file added test_data/golden/qr_with_border_radius.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions 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 added test_data/golden/qr_with_colors.pdf
Binary file not shown.
Binary file modified test_data/golden/qr_with_colors.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_colors.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_colors_rgba.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_colors_rgba.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_ec_level.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_empty_options.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_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.
Binary file modified test_data/golden/qr_with_margin.png
Binary file modified test_data/golden/qr_with_size.png
2 changes: 1 addition & 1 deletion test_data/golden/qr_with_size.svg

Large diffs are not rendered by default.

Binary file modified test_data/golden/qr_with_undefined_size.png

0 comments on commit 6f8b368

Please sign in to comment.