From 2fa12dafc536c977ce4e7e07a2a7c25d7fba3c02 Mon Sep 17 00:00:00 2001 From: Denys Kozak Date: Sat, 19 Oct 2024 14:15:34 +0300 Subject: [PATCH 1/3] Use tool getMimeType --- src/core/QRCodeStyling.ts | 13 +++++++------ src/core/QRSVG.ts | 3 ++- src/tools/getMimeType.ts | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 src/tools/getMimeType.ts diff --git a/src/core/QRCodeStyling.ts b/src/core/QRCodeStyling.ts index abe94399..83bf00ad 100644 --- a/src/core/QRCodeStyling.ts +++ b/src/core/QRCodeStyling.ts @@ -8,6 +8,7 @@ import defaultOptions, { RequiredOptions } from "./QROptions"; import sanitizeOptions from "../tools/sanitizeOptions"; import { FileExtension, QRCode, Options, DownloadOptions, ExtensionFunction, Window, Canvas } from "../types"; import qrcode from "qrcode-generator"; +import getMimeType from "../tools/getMimeType"; declare const window: Window; @@ -71,7 +72,7 @@ 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) => { @@ -183,7 +184,7 @@ export default class QRCodeStyling { const source = serializer.serializeToString(element); const svgString = `\r\n${source}`; if (typeof Blob !== "undefined" && !this._options.jsdom) { - return new Blob([svgString], { type: "image/svg+xml" }); + return new Blob([svgString], { type: getMimeType(extension) }); } else { return Buffer.from(svgString); } @@ -191,9 +192,9 @@ export default class QRCodeStyling { return new Promise((resolve) => { const canvas = element as Canvas; if (canvas.toBuffer) { - resolve(canvas.toBuffer(`image/${extension}`)); + resolve(canvas.toBuffer(getMimeType(extension))); } else { - canvas.toBlob(resolve, `image/${extension}`, 1); + canvas.toBlob(resolve, getMimeType(extension), 1); } }); } @@ -231,10 +232,10 @@ export default class QRCodeStyling { let source = serializer.serializeToString(element); source = '\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}`); } } diff --git a/src/core/QRSVG.ts b/src/core/QRSVG.ts index cc23ba23..f3946407 100644 --- a/src/core/QRSVG.ts +++ b/src/core/QRSVG.ts @@ -7,6 +7,7 @@ import { RequiredOptions } from "./QROptions"; import gradientTypes from "../constants/gradientTypes"; import shapeTypes from "../constants/shapeTypes"; import { QRCode, FilterFunction, Gradient, Window, Canvas } from "../types"; +import getMimeType from "../tools/getMimeType"; const squareMask = [ [1, 1, 1, 1, 1, 1, 1], @@ -462,7 +463,7 @@ export default class QRSVG { 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"); + this._imageUri = this._canvas.toDataURL(getMimeType('png')); } } } diff --git a/src/tools/getMimeType.ts b/src/tools/getMimeType.ts new file mode 100644 index 00000000..e5324a7f --- /dev/null +++ b/src/tools/getMimeType.ts @@ -0,0 +1,24 @@ +export default function getMimeType(extension: string) { + if (!extension) throw new Error('Extension must be defined'); + if (extension[0] === ".") { + extension = extension.substring(1); + } + const type = { + "bmp": "image/bmp", + "gif": "image/gif", + "ico": "image/vnd.microsoft.icon", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "png": "image/png", + "svg": "image/svg+xml", + "tif": "image/tiff", + "tiff": "image/tiff", + "webp": "image/webp", + }[extension.toLowerCase()] + + if (!type) { + throw new Error(`Extension "${extension}" is not supported`); + } + + return type; +} \ No newline at end of file From 7c170dbfb6de75eebee9303074d5a6fc412b7f57 Mon Sep 17 00:00:00 2001 From: Denys Kozak Date: Sat, 19 Oct 2024 21:30:19 +0300 Subject: [PATCH 2/3] Use nodeCanvas types from canvas library --- src/core/QRCodeStyling.ts | 58 ++++++---- src/core/QRSVG.ts | 41 ++++--- src/index.html | 233 ++++++++++++++++++++++++++------------ src/tools/getMimeType.ts | 1 + src/types/index.ts | 8 +- 5 files changed, 222 insertions(+), 119 deletions(-) diff --git a/src/core/QRCodeStyling.ts b/src/core/QRCodeStyling.ts index 83bf00ad..269fd8e4 100644 --- a/src/core/QRCodeStyling.ts +++ b/src/core/QRCodeStyling.ts @@ -6,9 +6,10 @@ 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; @@ -16,7 +17,8 @@ export default class QRCodeStyling { _options: RequiredOptions; _window: Window; _container?: HTMLElement; - _canvas?: Canvas; + _domCanvas?: HTMLCanvasElement; + _nodeCanvas?: NodeCanvas; _svg?: SVGElement; _qr?: QRCode; _extension?: ExtensionFunction; @@ -58,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(() => { @@ -75,18 +79,18 @@ export default class QRCodeStyling { 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(); }; @@ -96,7 +100,7 @@ export default class QRCodeStyling { }); } - async _getElement(extension: FileExtension = "png"): Promise { + async _getElement(extension: FileExtension = "png") { if (!this._qr) throw "QR code is empty"; if (extension.toLowerCase() === "svg") { @@ -106,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; } } @@ -145,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) { @@ -174,6 +178,7 @@ export default class QRCodeStyling { async getRawData(extension: FileExtension = "png"): Promise { if (!this._qr) throw "QR code is empty"; const element = await this._getElement(extension); + const mimeType = getMimeType(extension); if (!element) { return null; @@ -181,20 +186,29 @@ export default class QRCodeStyling { if (extension.toLowerCase() === "svg") { const serializer = new this._window.XMLSerializer(); - const source = serializer.serializeToString(element); + const source = serializer.serializeToString(element as SVGElement); const svgString = `\r\n${source}`; if (typeof Blob !== "undefined" && !this._options.jsdom) { - return new Blob([svgString], { type: getMimeType(extension) }); + 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(getMimeType(extension))); - } else { - canvas.toBlob(resolve, getMimeType(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); } }); } @@ -229,7 +243,7 @@ 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 = '\r\n' + source; const url = `data:${getMimeType(extension)};charset=utf-8,${encodeURIComponent(source)}`; diff --git a/src/core/QRSVG.ts b/src/core/QRSVG.ts index f3946407..52297fc7 100644 --- a/src/core/QRSVG.ts +++ b/src/core/QRSVG.ts @@ -6,8 +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 getMimeType from "../tools/getMimeType"; +import { QRCode, FilterFunction, Gradient, Window } from "../types"; +import { Canvas as NodeCanvas, Image } from "canvas"; const squareMask = [ [1, 1, 1, 1, 1, 1, 1], @@ -30,7 +30,8 @@ const dotMask = [ ]; export default class QRSVG { - _canvas?: Canvas; + _domCanvas?: HTMLCanvasElement; + _nodeCanvas?: NodeCanvas; _window: Window; _element: SVGElement; _defs: SVGElement; @@ -40,7 +41,7 @@ export default class QRSVG { _cornersDotClipPath?: SVGElement; _options: RequiredOptions; _qr?: QRCode; - _image?: HTMLImageElement; + _image?: HTMLImageElement | Image; _imageUri?: string; _instanceId: number; @@ -62,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++; @@ -104,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); @@ -457,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(getMimeType('png')); - } - } - } - loadImage(): Promise { return new Promise((resolve, reject) => { const options = this._options; @@ -479,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); @@ -498,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; diff --git a/src/index.html b/src/index.html index 9595c036..c734b651 100644 --- a/src/index.html +++ b/src/index.html @@ -1,76 +1,169 @@ - + - - - QR Code Styling - + + + QR Code Styling + + +
+
+
+ - + // qrCode1.download({ name: 'qr-codes/Vanbilloen'+'-color', extension: 'svg' }) + // qrCode2.download({ name: 'qr-codes/Vanbilloen'+'-linear', extension: 'svg' }) + // qrCode3.download({ name: 'qr-codes/Vanbilloen'+'-rounded', extension: 'svg' }) + + + diff --git a/src/tools/getMimeType.ts b/src/tools/getMimeType.ts index e5324a7f..1342c1b6 100644 --- a/src/tools/getMimeType.ts +++ b/src/tools/getMimeType.ts @@ -14,6 +14,7 @@ export default function getMimeType(extension: string) { "tif": "image/tiff", "tiff": "image/tiff", "webp": "image/webp", + "pdf": "application/pdf", }[extension.toLowerCase()] if (!type) { diff --git a/src/types/index.ts b/src/types/index.ts index 5aa112b7..c95e7195 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,5 @@ import { DOMWindow, JSDOM } from "jsdom"; +import nodeCanvas from "canvas"; export interface UnknownObject { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -13,11 +14,6 @@ export type GradientType = "radial" | "linear"; export type DrawType = "canvas" | "svg"; export type ShapeType = "square" | "circle"; -export interface Canvas extends HTMLCanvasElement { - toBuffer?: (type: string) => Buffer; - createCanvas?: (width: number, height: number) => Canvas; - loadImage?: (image: string) => Promise; -} export type Window = DOMWindow; export type Gradient = { @@ -120,7 +116,7 @@ export type Options = { margin?: number; data?: string; image?: string; - nodeCanvas?: Canvas; + nodeCanvas?: typeof nodeCanvas; jsdom?: typeof JSDOM; qrOptions?: { typeNumber?: TypeNumber; From efba7ff1ff5bc9a8512dae26c0a88e6eb79ea4e5 Mon Sep 17 00:00:00 2001 From: Denys Kozak Date: Sat, 19 Oct 2024 21:32:27 +0300 Subject: [PATCH 3/3] Change plugin link to npm --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b9d58be..6fb9dc8b 100644 --- a/README.md +++ b/README.md @@ -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)