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)

diff --git a/src/core/QRCodeStyling.ts b/src/core/QRCodeStyling.ts index abe94399..269fd8e4 100644 --- a/src/core/QRCodeStyling.ts +++ b/src/core/QRCodeStyling.ts @@ -6,8 +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; @@ -15,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; @@ -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(() => { @@ -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(); }; @@ -95,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") { @@ -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; } } @@ -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) { @@ -173,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; @@ -180,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: "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); } }); } @@ -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 = '\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..52297fc7 100644 --- a/src/core/QRSVG.ts +++ b/src/core/QRSVG.ts @@ -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], @@ -29,7 +30,8 @@ const dotMask = [ ]; export default class QRSVG { - _canvas?: Canvas; + _domCanvas?: HTMLCanvasElement; + _nodeCanvas?: NodeCanvas; _window: Window; _element: SVGElement; _defs: SVGElement; @@ -39,7 +41,7 @@ export default class QRSVG { _cornersDotClipPath?: SVGElement; _options: RequiredOptions; _qr?: QRCode; - _image?: HTMLImageElement; + _image?: HTMLImageElement | Image; _imageUri?: string; _instanceId: number; @@ -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++; @@ -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); @@ -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 { return new Promise((resolve, reject) => { const options = this._options; @@ -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); @@ -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; 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 new file mode 100644 index 00000000..1342c1b6 --- /dev/null +++ b/src/tools/getMimeType.ts @@ -0,0 +1,25 @@ +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", + "pdf": "application/pdf", + }[extension.toLowerCase()] + + if (!type) { + throw new Error(`Extension "${extension}" is not supported`); + } + + return type; +} \ No newline at end of file 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;