Skip to content

Commit

Permalink
Merge pull request #245 from kozakdenys/nodejs-types-fix
Browse files Browse the repository at this point in the history
nodeCanvas type fix for node.js env
  • Loading branch information
kozakdenys authored Oct 19, 2024
2 parents 5ad4e46 + efba7ff commit 8113995
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 122 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ If you have issues / suggestions / notes / questions, please open an issue or co
### Extensions

If you would like to use additional stiles, you can connect extensions.
#### [qr-border-plugin](https://www.lefe.dev/marketplace/qr-border-plugin)
#### [qr-border-plugin](https://www.npmjs.com/package/qr-border-plugin)

<p float="left">
<img style="display:inline-block" src="https://www.lefe.dev/_static/packages/qr-border-plugin-1.svg" width="240" />
Expand Down
65 changes: 40 additions & 25 deletions src/core/QRCodeStyling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import drawTypes from "../constants/drawTypes";

import defaultOptions, { RequiredOptions } from "./QROptions";
import sanitizeOptions from "../tools/sanitizeOptions";
import { FileExtension, QRCode, Options, DownloadOptions, ExtensionFunction, Window, Canvas } from "../types";
import { FileExtension, QRCode, Options, DownloadOptions, ExtensionFunction, Window } from "../types";
import qrcode from "qrcode-generator";
import getMimeType from "../tools/getMimeType";
import { Canvas as NodeCanvas, Image } from "canvas";

declare const window: Window;

export default class QRCodeStyling {
_options: RequiredOptions;
_window: Window;
_container?: HTMLElement;
_canvas?: Canvas;
_domCanvas?: HTMLCanvasElement;
_nodeCanvas?: NodeCanvas;
_svg?: SVGElement;
_qr?: QRCode;
_extension?: ExtensionFunction;
Expand Down Expand Up @@ -57,12 +60,14 @@ export default class QRCodeStyling {
}

if (this._options.nodeCanvas?.createCanvas) {
this._canvas = this._options.nodeCanvas.createCanvas(this._options.width, this._options.height);
this._nodeCanvas = this._options.nodeCanvas.createCanvas(this._options.width, this._options.height);
this._nodeCanvas.width = this._options.width;
this._nodeCanvas.height = this._options.height;
} else {
this._canvas = document.createElement("canvas");
this._domCanvas = document.createElement("canvas");
this._domCanvas.width = this._options.width;
this._domCanvas.height = this._options.height;
}
this._canvas.width = this._options.width;
this._canvas.height = this._options.height;

this._setupSvg();
this._canvasDrawingPromise = this._svgDrawingPromise?.then(() => {
Expand All @@ -71,21 +76,21 @@ export default class QRCodeStyling {
const svg = this._svg;
const xml = new this._window.XMLSerializer().serializeToString(svg);
const svg64 = btoa(xml);
const image64 = "data:image/svg+xml;base64," + svg64;
const image64 = `data:${getMimeType('svg')};base64,${svg64}`;

if (this._options.nodeCanvas?.loadImage) {
return this._options.nodeCanvas.loadImage(image64).then((image: HTMLImageElement) => {
return this._options.nodeCanvas.loadImage(image64).then((image: Image) => {
// fix blurry svg
image.width = this._options.width;
image.height = this._options.height;
this._canvas?.getContext("2d")?.drawImage(image, 0, 0);
this._nodeCanvas?.getContext("2d")?.drawImage(image, 0, 0);
});
} else {
const image = new this._window.Image();

return new Promise((resolve) => {
image.onload = (): void => {
this._canvas?.getContext("2d")?.drawImage(image, 0, 0);
this._domCanvas?.getContext("2d")?.drawImage(image, 0, 0);
resolve();
};

Expand All @@ -95,7 +100,7 @@ export default class QRCodeStyling {
});
}

async _getElement(extension: FileExtension = "png"): Promise<SVGElement | Canvas | undefined> {
async _getElement(extension: FileExtension = "png") {
if (!this._qr) throw "QR code is empty";

if (extension.toLowerCase() === "svg") {
Expand All @@ -105,11 +110,11 @@ export default class QRCodeStyling {
await this._svgDrawingPromise;
return this._svg;
} else {
if (!this._canvas || !this._canvasDrawingPromise) {
if (!(this._domCanvas || this._nodeCanvas) || !this._canvasDrawingPromise) {
this._setupCanvas();
}
await this._canvasDrawingPromise;
return this._canvas;
return this._domCanvas || this._nodeCanvas;
}
}

Expand Down Expand Up @@ -144,8 +149,8 @@ export default class QRCodeStyling {
}

if (this._options.type === drawTypes.canvas) {
if (this._canvas) {
container.appendChild(this._canvas);
if (this._domCanvas) {
container.appendChild(this._domCanvas);
}
} else {
if (this._svg) {
Expand Down Expand Up @@ -173,27 +178,37 @@ export default class QRCodeStyling {
async getRawData(extension: FileExtension = "png"): Promise<Blob | Buffer | null> {
if (!this._qr) throw "QR code is empty";
const element = await this._getElement(extension);
const mimeType = getMimeType(extension);

if (!element) {
return null;
}

if (extension.toLowerCase() === "svg") {
const serializer = new this._window.XMLSerializer();
const source = serializer.serializeToString(element);
const source = serializer.serializeToString(element as SVGElement);
const svgString = `<?xml version="1.0" standalone="no"?>\r\n${source}`;
if (typeof Blob !== "undefined" && !this._options.jsdom) {
return new Blob([svgString], { type: "image/svg+xml" });
return new Blob([svgString], { type: mimeType });
} else {
return Buffer.from(svgString);
}
} else {
return new Promise((resolve) => {
const canvas = element as Canvas;
if (canvas.toBuffer) {
resolve(canvas.toBuffer(`image/${extension}`));
} else {
canvas.toBlob(resolve, `image/${extension}`, 1);
const canvas = element;
if ('toBuffer' in canvas) {
// Different call is needed to prevent error TS2769: No overload matches this call.
if (mimeType === "image/png") {
resolve(canvas.toBuffer(mimeType));
} else if (mimeType === "image/jpeg") {
resolve(canvas.toBuffer(mimeType));
} else if (mimeType === "application/pdf") {
resolve(canvas.toBuffer(mimeType));
} else {
throw Error("Unsupported extension");
}
} else if ('toBlob' in canvas) {
(canvas).toBlob(resolve, mimeType, 1);
}
});
}
Expand Down Expand Up @@ -228,13 +243,13 @@ export default class QRCodeStyling {

if (extension.toLowerCase() === "svg") {
const serializer = new XMLSerializer();
let source = serializer.serializeToString(element);
let source = serializer.serializeToString(element as SVGElement);

source = '<?xml version="1.0" standalone="no"?>\r\n' + source;
const url = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
const url = `data:${getMimeType(extension)};charset=utf-8,${encodeURIComponent(source)}`;
downloadURI(url, `${name}.svg`);
} else {
const url = (element as HTMLCanvasElement).toDataURL(`image/${extension}`);
const url = (element as HTMLCanvasElement).toDataURL(getMimeType(extension));
downloadURI(url, `${name}.${extension}`);
}
}
Expand Down
40 changes: 20 additions & 20 deletions src/core/QRSVG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import QRCornerDot from "../figures/cornerDot/QRCornerDot";
import { RequiredOptions } from "./QROptions";
import gradientTypes from "../constants/gradientTypes";
import shapeTypes from "../constants/shapeTypes";
import { QRCode, FilterFunction, Gradient, Window, Canvas } from "../types";
import { QRCode, FilterFunction, Gradient, Window } from "../types";
import { Canvas as NodeCanvas, Image } from "canvas";

const squareMask = [
[1, 1, 1, 1, 1, 1, 1],
Expand All @@ -29,7 +30,8 @@ const dotMask = [
];

export default class QRSVG {
_canvas?: Canvas;
_domCanvas?: HTMLCanvasElement;
_nodeCanvas?: NodeCanvas;
_window: Window;
_element: SVGElement;
_defs: SVGElement;
Expand All @@ -39,7 +41,7 @@ export default class QRSVG {
_cornersDotClipPath?: SVGElement;
_options: RequiredOptions;
_qr?: QRCode;
_image?: HTMLImageElement;
_image?: HTMLImageElement | Image;
_imageUri?: string;
_instanceId: number;

Expand All @@ -61,12 +63,14 @@ export default class QRSVG {

if (options.imageOptions.saveAsBlob) {
if (options.nodeCanvas?.createCanvas) {
this._canvas = options.nodeCanvas.createCanvas(options.width, options.height);
this._nodeCanvas = options.nodeCanvas.createCanvas(options.width, options.height);
this._nodeCanvas.width = options.width;
this._nodeCanvas.height = options.height;
} else {
this._canvas = document.createElement("canvas");
this._domCanvas = document.createElement("canvas");
this._domCanvas.width = options.width;
this._domCanvas.height = options.height;
}
this._canvas.width = options.width;
this._canvas.height = options.height;
}
this._imageUri = options.image;
this._instanceId = QRSVG.instanceCount++;
Expand Down Expand Up @@ -103,7 +107,6 @@ export default class QRSVG {
//We need it to get image size
await this.loadImage();
if (!this._image) return;
this.imageToBlob();
const { imageOptions, qrOptions } = this._options;
const coverLevel = imageOptions.imageSize * errorCorrectionPercents[qrOptions.errorCorrectionLevel];
const maxHiddenDots = Math.floor(coverLevel * count * count);
Expand Down Expand Up @@ -456,17 +459,6 @@ export default class QRSVG {
});
}

imageToBlob(): void {
if (!this._image) return;
if (this._options.imageOptions.saveAsBlob && this._canvas) {
const ctx = this._canvas.getContext("2d");
if (ctx) {
ctx.drawImage(this._image, 0, 0, this._canvas.width, this._canvas.height);
this._imageUri = this._canvas.toDataURL("image/png");
}
}
}

loadImage(): Promise<void> {
return new Promise((resolve, reject) => {
const options = this._options;
Expand All @@ -478,13 +470,17 @@ export default class QRSVG {
if (options.nodeCanvas?.loadImage) {
options.nodeCanvas
.loadImage(options.image)
.then((image: HTMLImageElement) => {
.then((image: Image) => {
// fix blurry svg
if (/(\.svg$)|(^data:image\/svg)/.test(options.image ?? "")) {
image.width = this._options.width;
image.height = this._options.height;
}
this._image = image;
if (this._options.imageOptions.saveAsBlob && this._nodeCanvas) {
this._nodeCanvas.getContext('2d')?.drawImage(image, 0, 0, this._nodeCanvas.width, this._nodeCanvas.height);
this._imageUri = this._nodeCanvas.toDataURL('image/png');
}
resolve();
})
.catch(reject);
Expand All @@ -497,6 +493,10 @@ export default class QRSVG {

this._image = image;
image.onload = (): void => {
if (this._options.imageOptions.saveAsBlob && this._domCanvas) {
this._domCanvas.getContext('2d')?.drawImage(image, 0, 0, this._domCanvas.width, this._domCanvas.height);
this._imageUri = this._domCanvas.toDataURL('image/png');
}
resolve();
};
image.src = options.image;
Expand Down
Loading

0 comments on commit 8113995

Please sign in to comment.