UNPKG

awesome-qr

Version:

An awesome but simple QR code generator written in JavaScript.

879 lines (772 loc) 28.6 kB
import { Canvas, CanvasRenderingContext2D, createCanvas, Image, loadImage } from "canvas"; import { decompressFrames, parseGIF } from "./gifuct-js"; import { QRCodeModel, QRErrorCorrectLevel, QRUtil } from "./qrcode"; import GIFEncoder from "./gif.js/GIFEncoder"; const defaultScale = 0.4; export type ComponentOptions = { /** * Component options for data/ECC. */ data?: { /** * Scale factor for data/ECC dots. * @default 1 */ scale?: number; }; /** * Component options for timing patterns. */ timing?: { /** * Scale factor for timing patterns. * @default 1 */ scale?: number; /** * Protector for timing patterns. * @default false */ protectors?: boolean; }; /** * Component options for alignment patterns. */ alignment?: { /** * Scale factor for alignment patterns. * @default 1 */ scale?: number; /** * Protector for alignment patterns. * @default false */ protectors?: boolean; }; /** * Component options for alignment pattern on the bottom-right corner. */ cornerAlignment?: { /** * Scale factor for alignment pattern on the bottom-right corner. * @default 1 */ scale?: number; /** * Protector for alignment pattern on the bottom-right corner. * @default true */ protectors?: boolean; }; }; export type Options = { /** * Text to be encoded in the QR code. */ text: string; /** * Size of the QR code in pixel. * * @defaultValue 400 */ size?: number; /** * Size of margins around the QR code body in pixel. * * @defaultValue 20 */ margin?: number; /** * Error correction level of the QR code. * * Accepts a value provided by _QRErrorCorrectLevel_. * * For more information, please refer to [https://www.qrcode.com/en/about/error_correction.html](https://www.qrcode.com/en/about/error_correction.html). * * @defaultValue 0 */ correctLevel?: number; /** * **This is an advanced option.** * * Specify the mask pattern to be used in QR code encoding. * * Accepts a value provided by _QRMaskPattern_. * * To find out all eight mask patterns, please refer to [https://en.wikipedia.org/wiki/File:QR_Code_Mask_Patterns.svg](https://en.wikipedia.org/wiki/File:QR_Code_Mask_Patterns.svg) * * For more information, please refer to [https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders#Masking](https://en.wikiversity.org/wiki/Reed%E2%80%93Solomon_codes_for_coders#Masking). */ maskPattern?: number; /** * **This is an advanced option.** * * Specify the version to be used in QR code encoding. * * Accepts an integer in range [1, 40]. * * For more information, please refer to [https://www.qrcode.com/en/about/version.html](https://www.qrcode.com/en/about/version.html). */ version?: number; /** * Options to control components in the QR code. * * @deafultValue undefined */ components?: ComponentOptions; /** * Color of the blocks on the QR code. * * Accepts a CSS &lt;color&gt;. * * For more information about CSS &lt;color&gt;, please refer to [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). * * @defaultValue "#000000" */ colorDark?: string; /** * Color of the empty areas on the QR code. * * Accepts a CSS &lt;color&gt;. * * For more information about CSS &lt;color&gt;, please refer to [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). * * @defaultValue "#ffffff" */ colorLight?: string; /** * Automatically calculate the _colorLight_ value from the QR code's background. * * @defaultValue true */ autoColor?: boolean; /** * Background image to be used in the QR code. * * Accepts a `data:` string in web browsers or a Buffer in Node.js. * * @defaultValue undefined */ backgroundImage?: string | Buffer; /** * Color of the dimming mask above the background image. * * Accepts a CSS &lt;color&gt;. * * For more information about CSS &lt;color&gt;, please refer to [https://developer.mozilla.org/en-US/docs/Web/CSS/color_value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value). * * @defaultValue "rgba(0, 0, 0, 0)" */ backgroundDimming?: string; /** * GIF background image to be used in the QR code. * * @defaultValue undefined */ gifBackground?: ArrayBuffer; /** * Use a white margin instead of a transparent one which reveals the background of the QR code on margins. * * @defaultValue true */ whiteMargin?: boolean; /** * Logo image to be displayed at the center of the QR code. * * Accepts a `data:` string in web browsers or a Buffer in Node.js. * * When set to `undefined` or `null`, the logo is disabled. * * @defaultValue undefined */ logoImage?: string | Buffer; /** * Ratio of the logo size to the QR code size. * * @defaultValue 0.2 */ logoScale?: number; /** * Size of margins around the logo image in pixels. * * @defaultValue 6 */ logoMargin?: number; /** * Corner radius of the logo image in pixels. * * @defaultValue 8 */ logoCornerRadius?: number; /** * @deprecated * * Ratio of the real size to the full size of the blocks. * * This can be helpful when you want to make more parts of the background visible. * * @deafultValue 0.4 */ dotScale?: number; }; export class AwesomeQR { private canvas: Canvas; private canvasContext: CanvasRenderingContext2D; private qrCode?: QRCodeModel; private options: Options; static CorrectLevel = QRErrorCorrectLevel; private static defaultComponentOptions: ComponentOptions = { data: { scale: 1, }, timing: { scale: 1, protectors: false, }, alignment: { scale: 1, protectors: false, }, cornerAlignment: { scale: 1, protectors: true, }, }; static defaultOptions: Options = { text: "", size: 400, margin: 20, colorDark: "#000000", colorLight: "#ffffff", correctLevel: QRErrorCorrectLevel.M, backgroundImage: undefined, backgroundDimming: "rgba(0,0,0,0)", logoImage: undefined, logoScale: 0.2, logoMargin: 4, logoCornerRadius: 8, whiteMargin: true, components: AwesomeQR.defaultComponentOptions, autoColor: true, }; constructor(options: Partial<Options>) { const _options = Object.assign({}, options); (Object.keys(AwesomeQR.defaultOptions) as (keyof Options)[]).forEach((k) => { if (!(k in _options)) { Object.defineProperty(_options, k, { value: AwesomeQR.defaultOptions[k], enumerable: true, writable: true }); } }); if (!_options.components) { _options.components = AwesomeQR.defaultComponentOptions; } else if (typeof _options.components === "object") { (Object.keys(AwesomeQR.defaultComponentOptions) as (keyof ComponentOptions)[]).forEach((k) => { if (!(k in _options.components!)) { Object.defineProperty(_options.components!, k, { value: AwesomeQR.defaultComponentOptions[k], enumerable: true, writable: true, }); } else { Object.defineProperty(_options.components!, k, { value: { ...AwesomeQR.defaultComponentOptions[k], ..._options.components![k] }, enumerable: true, writable: true, }); } }); } if (_options.dotScale !== null && _options.dotScale !== undefined) { if (_options.dotScale! <= 0 || _options.dotScale! > 1) { throw new Error("dotScale should be in range (0, 1]."); } _options.components.data!.scale = _options.dotScale; _options.components.timing!.scale = _options.dotScale; _options.components.alignment!.scale = _options.dotScale; } this.options = _options as Options; this.canvas = createCanvas(options.size!, options.size!); this.canvasContext = this.canvas.getContext("2d")!; this.qrCode = new QRCodeModel(-1, this.options.correctLevel!); if (Number.isInteger(this.options.maskPattern)) { this.qrCode.maskPattern = this.options.maskPattern!; } if (Number.isInteger(this.options.version)) { this.qrCode.typeNumber = this.options.version!; } this.qrCode.addData(this.options.text); this.qrCode.make(); } draw(): Promise<Buffer | ArrayBuffer | string | undefined> { return new Promise((resolve) => this._draw().then(resolve)); } private _clear() { this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height); } private static _prepareRoundedCornerClip( canvasContext: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number ) { canvasContext.beginPath(); canvasContext.moveTo(x, y); canvasContext.arcTo(x + w, y, x + w, y + h, r); canvasContext.arcTo(x + w, y + h, x, y + h, r); canvasContext.arcTo(x, y + h, x, y, r); canvasContext.arcTo(x, y, x + w, y, r); canvasContext.closePath(); } private static _getAverageRGB(image: Image) { const blockSize = 5; const defaultRGB = { r: 0, g: 0, b: 0, }; let width, height; let i = -4; const rgb = { r: 0, g: 0, b: 0, }; let count = 0; height = image.naturalHeight || image.height; width = image.naturalWidth || image.width; const canvas = createCanvas(width, height); const context = canvas.getContext("2d"); if (!context) { return defaultRGB; } context.drawImage(image, 0, 0); let data; try { data = context.getImageData(0, 0, width, height); } catch (e) { return defaultRGB; } while ((i += blockSize * 4) < data.data.length) { if (data.data[i] > 200 || data.data[i + 1] > 200 || data.data[i + 2] > 200) continue; ++count; rgb.r += data.data[i]; rgb.g += data.data[i + 1]; rgb.b += data.data[i + 2]; } rgb.r = ~~(rgb.r / count); rgb.g = ~~(rgb.g / count); rgb.b = ~~(rgb.b / count); return rgb; } private static _drawDot( canvasContext: CanvasRenderingContext2D, centerX: number, centerY: number, nSize: number, xyOffset: number = 0, dotScale: number = 1 ) { canvasContext.fillRect( (centerX + xyOffset) * nSize, (centerY + xyOffset) * nSize, dotScale * nSize, dotScale * nSize ); } private static _drawAlignProtector( canvasContext: CanvasRenderingContext2D, centerX: number, centerY: number, nSize: number ) { canvasContext.clearRect((centerX - 2) * nSize, (centerY - 2) * nSize, 5 * nSize, 5 * nSize); canvasContext.fillRect((centerX - 2) * nSize, (centerY - 2) * nSize, 5 * nSize, 5 * nSize); } private static _drawAlign( canvasContext: CanvasRenderingContext2D, centerX: number, centerY: number, nSize: number, xyOffset: number = 0, dotScale: number = 1, colorDark: string, hasProtector: boolean ) { const oldFillStyle = canvasContext.fillStyle; canvasContext.fillStyle = colorDark; new Array(4).fill(0).map((_, i) => { AwesomeQR._drawDot(canvasContext, centerX - 2 + i, centerY - 2, nSize, xyOffset, dotScale); AwesomeQR._drawDot(canvasContext, centerX + 2, centerY - 2 + i, nSize, xyOffset, dotScale); AwesomeQR._drawDot(canvasContext, centerX + 2 - i, centerY + 2, nSize, xyOffset, dotScale); AwesomeQR._drawDot(canvasContext, centerX - 2, centerY + 2 - i, nSize, xyOffset, dotScale); }); AwesomeQR._drawDot(canvasContext, centerX, centerY, nSize, xyOffset, dotScale); if (!hasProtector) { canvasContext.fillStyle = "rgba(255, 255, 255, 0.6)"; new Array(2).fill(0).map((_, i) => { AwesomeQR._drawDot(canvasContext, centerX - 1 + i, centerY - 1, nSize, xyOffset, dotScale); AwesomeQR._drawDot(canvasContext, centerX + 1, centerY - 1 + i, nSize, xyOffset, dotScale); AwesomeQR._drawDot(canvasContext, centerX + 1 - i, centerY + 1, nSize, xyOffset, dotScale); AwesomeQR._drawDot(canvasContext, centerX - 1, centerY + 1 - i, nSize, xyOffset, dotScale); }); } canvasContext.fillStyle = oldFillStyle; } private async _draw(): Promise<Buffer | Uint8Array | string | undefined> { const nCount = this.qrCode?.moduleCount!; const rawSize = this.options.size!; let rawMargin = this.options.margin!; if (rawMargin < 0 || rawMargin * 2 >= rawSize) { rawMargin = 0; } const margin = Math.ceil(rawMargin); const rawViewportSize = rawSize - 2 * rawMargin; const whiteMargin = this.options.whiteMargin!; const backgroundDimming = this.options.backgroundDimming!; const nSize = Math.ceil(rawViewportSize / nCount); const viewportSize = nSize * nCount; const size = viewportSize + 2 * margin; const mainCanvas = createCanvas(size, size); const mainCanvasContext = mainCanvas.getContext("2d"); this._clear(); // Translate to make the top and left margins off the viewport mainCanvasContext.save(); mainCanvasContext.translate(margin, margin); const backgroundCanvas = createCanvas(size, size); const backgroundCanvasContext = backgroundCanvas.getContext("2d"); let parsedGIFBackground = null; let gifFrames: any[] = []; if (!!this.options.gifBackground) { const gif = parseGIF(this.options.gifBackground); parsedGIFBackground = gif; gifFrames = decompressFrames(gif, true); if (this.options.autoColor) { let r = 0, g = 0, b = 0; let count = 0; for (let i = 0; i < gifFrames[0].colorTable.length; i++) { const c = gifFrames[0].colorTable[i]; if (c[0] > 200 || c[1] > 200 || c[2] > 200) continue; if (c[0] === 0 && c[1] === 0 && c[2] === 0) continue; count++; r += c[0]; g += c[1]; b += c[2]; } r = ~~(r / count); g = ~~(g / count); b = ~~(b / count); this.options.colorDark = `rgb(${r},${g},${b})`; } } else if (!!this.options.backgroundImage) { const backgroundImage = await loadImage(this.options.backgroundImage!); if (this.options.autoColor) { const avgRGB = AwesomeQR._getAverageRGB(backgroundImage); this.options.colorDark = `rgb(${avgRGB.r},${avgRGB.g},${avgRGB.b})`; } backgroundCanvasContext.drawImage( backgroundImage, 0, 0, backgroundImage.width, backgroundImage.height, 0, 0, size, size ); backgroundCanvasContext.rect(0, 0, size, size); backgroundCanvasContext.fillStyle = backgroundDimming; backgroundCanvasContext.fill(); } else { backgroundCanvasContext.rect(0, 0, size, size); backgroundCanvasContext.fillStyle = this.options.colorLight!; backgroundCanvasContext.fill(); } const alignmentPatternCenters = QRUtil.getPatternPosition(this.qrCode!.typeNumber); const dataScale = this.options.components?.data?.scale || defaultScale; const dataXyOffset = (1 - dataScale) * 0.5; for (let row = 0; row < nCount; row++) { for (let col = 0; col < nCount; col++) { const bIsDark = this.qrCode!.isDark(row, col); const isBlkPosCtr = (col < 8 && (row < 8 || row >= nCount - 8)) || (col >= nCount - 8 && row < 8); const isTiming = (row == 6 && col >= 8 && col <= nCount - 8) || (col == 6 && row >= 8 && row <= nCount - 8); let isProtected = isBlkPosCtr || isTiming; for (let i = 1; i < alignmentPatternCenters.length - 1; i++) { isProtected = isProtected || (row >= alignmentPatternCenters[i] - 2 && row <= alignmentPatternCenters[i] + 2 && col >= alignmentPatternCenters[i] - 2 && col <= alignmentPatternCenters[i] + 2); } const nLeft = col * nSize + (isProtected ? 0 : dataXyOffset * nSize); const nTop = row * nSize + (isProtected ? 0 : dataXyOffset * nSize); mainCanvasContext.strokeStyle = bIsDark ? this.options.colorDark! : this.options.colorLight!; mainCanvasContext.lineWidth = 0.5; mainCanvasContext.fillStyle = bIsDark ? this.options.colorDark! : "rgba(255, 255, 255, 0.6)"; if (alignmentPatternCenters.length === 0) { if (!isProtected) { mainCanvasContext.fillRect( nLeft, nTop, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize ); } } else { const inAgnRange = col < nCount - 4 && col >= nCount - 4 - 5 && row < nCount - 4 && row >= nCount - 4 - 5; if (!isProtected && !inAgnRange) { // if align pattern list is empty, then it means that we don't need to leave room for the align patterns mainCanvasContext.fillRect( nLeft, nTop, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize ); } } } } const cornerAlignmentCenter = alignmentPatternCenters[alignmentPatternCenters.length - 1]; // - PROTECTORS const protectorStyle = "rgba(255, 255, 255, 0.6)"; // - FINDER PROTECTORS mainCanvasContext.fillStyle = protectorStyle; mainCanvasContext.fillRect(0, 0, 8 * nSize, 8 * nSize); mainCanvasContext.fillRect(0, (nCount - 8) * nSize, 8 * nSize, 8 * nSize); mainCanvasContext.fillRect((nCount - 8) * nSize, 0, 8 * nSize, 8 * nSize); // - TIMING PROTECTORS if (this.options.components?.timing?.protectors) { mainCanvasContext.fillRect(8 * nSize, 6 * nSize, (nCount - 8 - 8) * nSize, nSize); mainCanvasContext.fillRect(6 * nSize, 8 * nSize, nSize, (nCount - 8 - 8) * nSize); } // - CORNER ALIGNMENT PROTECTORS if (this.options.components?.cornerAlignment?.protectors) { AwesomeQR._drawAlignProtector(mainCanvasContext, cornerAlignmentCenter, cornerAlignmentCenter, nSize); } // - ALIGNMENT PROTECTORS if (this.options.components?.alignment?.protectors) { for (let i = 0; i < alignmentPatternCenters.length; i++) { for (let j = 0; j < alignmentPatternCenters.length; j++) { const agnX = alignmentPatternCenters[j]; const agnY = alignmentPatternCenters[i]; if (agnX === 6 && (agnY === 6 || agnY === cornerAlignmentCenter)) { continue; } else if (agnY === 6 && (agnX === 6 || agnX === cornerAlignmentCenter)) { continue; } else if (agnX === cornerAlignmentCenter && agnY === cornerAlignmentCenter) { continue; } else { AwesomeQR._drawAlignProtector(mainCanvasContext, agnX, agnY, nSize); } } } } // - FINDER mainCanvasContext.fillStyle = this.options.colorDark!; mainCanvasContext.fillRect(0, 0, 7 * nSize, nSize); mainCanvasContext.fillRect((nCount - 7) * nSize, 0, 7 * nSize, nSize); mainCanvasContext.fillRect(0, 6 * nSize, 7 * nSize, nSize); mainCanvasContext.fillRect((nCount - 7) * nSize, 6 * nSize, 7 * nSize, nSize); mainCanvasContext.fillRect(0, (nCount - 7) * nSize, 7 * nSize, nSize); mainCanvasContext.fillRect(0, (nCount - 7 + 6) * nSize, 7 * nSize, nSize); mainCanvasContext.fillRect(0, 0, nSize, 7 * nSize); mainCanvasContext.fillRect(6 * nSize, 0, nSize, 7 * nSize); mainCanvasContext.fillRect((nCount - 7) * nSize, 0, nSize, 7 * nSize); mainCanvasContext.fillRect((nCount - 7 + 6) * nSize, 0, nSize, 7 * nSize); mainCanvasContext.fillRect(0, (nCount - 7) * nSize, nSize, 7 * nSize); mainCanvasContext.fillRect(6 * nSize, (nCount - 7) * nSize, nSize, 7 * nSize); mainCanvasContext.fillRect(2 * nSize, 2 * nSize, 3 * nSize, 3 * nSize); mainCanvasContext.fillRect((nCount - 7 + 2) * nSize, 2 * nSize, 3 * nSize, 3 * nSize); mainCanvasContext.fillRect(2 * nSize, (nCount - 7 + 2) * nSize, 3 * nSize, 3 * nSize); // - TIMING const timingScale = this.options.components?.timing?.scale || defaultScale; const timingXyOffset = (1 - timingScale) * 0.5; for (let i = 0; i < nCount - 8; i += 2) { AwesomeQR._drawDot(mainCanvasContext, 8 + i, 6, nSize, timingXyOffset, timingScale); AwesomeQR._drawDot(mainCanvasContext, 6, 8 + i, nSize, timingXyOffset, timingScale); } // - CORNER ALIGNMENT PROTECTORS const cornerAlignmentScale = this.options.components?.cornerAlignment?.scale || defaultScale; const cornerAlignmentXyOffset = (1 - cornerAlignmentScale) * 0.5; AwesomeQR._drawAlign( mainCanvasContext, cornerAlignmentCenter, cornerAlignmentCenter, nSize, cornerAlignmentXyOffset, cornerAlignmentScale, this.options.colorDark!, this.options.components?.cornerAlignment?.protectors || false ); // - ALIGNEMNT const alignmentScale = this.options.components?.alignment?.scale || defaultScale; const alignmentXyOffset = (1 - alignmentScale) * 0.5; for (let i = 0; i < alignmentPatternCenters.length; i++) { for (let j = 0; j < alignmentPatternCenters.length; j++) { const agnX = alignmentPatternCenters[j]; const agnY = alignmentPatternCenters[i]; if (agnX === 6 && (agnY === 6 || agnY === cornerAlignmentCenter)) { continue; } else if (agnY === 6 && (agnX === 6 || agnX === cornerAlignmentCenter)) { continue; } else if (agnX === cornerAlignmentCenter && agnY === cornerAlignmentCenter) { continue; } else { AwesomeQR._drawAlign( mainCanvasContext, agnX, agnY, nSize, alignmentXyOffset, alignmentScale, this.options.colorDark!, this.options.components?.alignment?.protectors || false ); } } } // Fill the margin if (whiteMargin) { mainCanvasContext.fillStyle = "#FFFFFF"; mainCanvasContext.fillRect(-margin, -margin, size, margin); mainCanvasContext.fillRect(-margin, viewportSize, size, margin); mainCanvasContext.fillRect(viewportSize, -margin, margin, size); mainCanvasContext.fillRect(-margin, -margin, margin, size); } if (!!this.options.logoImage) { const logoImage = await loadImage(this.options.logoImage!); let logoScale = this.options.logoScale!; let logoMargin = this.options.logoMargin!; let logoCornerRadius = this.options.logoCornerRadius!; if (logoScale <= 0 || logoScale >= 1.0) { logoScale = 0.2; } if (logoMargin < 0) { logoMargin = 0; } if (logoCornerRadius < 0) { logoCornerRadius = 0; } const logoSize = viewportSize * logoScale; const x = 0.5 * (size - logoSize); const y = x; // Restore the canvas // After restoring, the top and left margins should be taken into account mainCanvasContext.restore(); // Clean the area that the logo covers (including margins) mainCanvasContext.fillStyle = "#FFFFFF"; mainCanvasContext.save(); AwesomeQR._prepareRoundedCornerClip( mainCanvasContext, x - logoMargin, y - logoMargin, logoSize + 2 * logoMargin, logoSize + 2 * logoMargin, logoCornerRadius + logoMargin ); mainCanvasContext.clip(); const oldGlobalCompositeOperation = mainCanvasContext.globalCompositeOperation; mainCanvasContext.globalCompositeOperation = "destination-out"; mainCanvasContext.fill(); mainCanvasContext.globalCompositeOperation = oldGlobalCompositeOperation; mainCanvasContext.restore(); // Draw logo image mainCanvasContext.save(); AwesomeQR._prepareRoundedCornerClip(mainCanvasContext, x, y, logoSize, logoSize, logoCornerRadius); mainCanvasContext.clip(); mainCanvasContext.drawImage(logoImage, x, y, logoSize, logoSize); mainCanvasContext.restore(); // Re-translate the canvas to translate the top and left margins into invisible area mainCanvasContext.save(); mainCanvasContext.translate(margin, margin); } if (!!parsedGIFBackground) { let gifOutput: any; // Reuse in order to apply the patch let backgroundCanvas: Canvas; let backgroundCanvasContext: CanvasRenderingContext2D; let patchCanvas: Canvas; let patchCanvasContext: CanvasRenderingContext2D; let patchData: any; gifFrames.forEach(function (frame: any) { if (!gifOutput) { gifOutput = new GIFEncoder(rawSize, rawSize); gifOutput.setDelay(frame.delay); gifOutput.setRepeat(0); } const { width, height } = frame.dims; if (!backgroundCanvas) { backgroundCanvas = createCanvas(width, height); backgroundCanvasContext = backgroundCanvas.getContext("2d"); backgroundCanvasContext.rect(0, 0, backgroundCanvas.width, backgroundCanvas.height); backgroundCanvasContext.fillStyle = "#ffffff"; backgroundCanvasContext.fill(); } if (!patchCanvas || !patchData || width !== patchCanvas.width || height !== patchCanvas.height) { patchCanvas = createCanvas(width, height); patchCanvasContext = patchCanvas.getContext("2d"); patchData = patchCanvasContext.createImageData(width, height); } patchData.data.set(frame.patch); patchCanvasContext.putImageData(patchData, 0, 0); backgroundCanvasContext.drawImage(patchCanvas, frame.dims.left, frame.dims.top); const unscaledCanvas = createCanvas(size, size); const unscaledCanvasContext = unscaledCanvas.getContext("2d"); unscaledCanvasContext.drawImage(backgroundCanvas, 0, 0, size, size); unscaledCanvasContext.rect(0, 0, size, size); unscaledCanvasContext.fillStyle = backgroundDimming; unscaledCanvasContext.fill(); unscaledCanvasContext.drawImage(mainCanvas, 0, 0, size, size); // Scale the final image const outCanvas = createCanvas(rawSize, rawSize); const outCanvasContext = outCanvas.getContext("2d"); outCanvasContext.drawImage(unscaledCanvas, 0, 0, rawSize, rawSize); gifOutput.addFrame(outCanvasContext.getImageData(0, 0, outCanvas.width, outCanvas.height).data); }); if (!gifOutput) { throw new Error("No frames."); } gifOutput.finish(); if (isElement(this.canvas)) { const u8array = gifOutput.stream().toFlattenUint8Array(); const binary = u8array.reduce((bin: string, u8: number) => bin + String.fromCharCode(u8), ""); return Promise.resolve(`data:image/gif;base64,${window.btoa(binary)}`); } return Promise.resolve(Buffer.from(gifOutput.stream().toFlattenUint8Array())); } else { // Swap and merge the foreground and the background backgroundCanvasContext.drawImage(mainCanvas, 0, 0, size, size); mainCanvasContext.drawImage(backgroundCanvas, -margin, -margin, size, size); // Scale the final image const outCanvas = createCanvas(rawSize, rawSize); //document.createElement("canvas"); const outCanvasContext = outCanvas.getContext("2d"); outCanvasContext.drawImage(mainCanvas, 0, 0, rawSize, rawSize); this.canvas = outCanvas; if (isElement(this.canvas)) { return Promise.resolve(this.canvas.toDataURL()); } return Promise.resolve(this.canvas.toBuffer()); } } } function isElement(obj: any): boolean { try { //Using W3 DOM2 (works for FF, Opera and Chrome) return obj instanceof HTMLElement; } catch (e) { //Browsers not supporting W3 DOM2 don't have HTMLElement and //an exception is thrown and we end up here. Testing some //properties that all elements have (works on IE7) return ( typeof obj === "object" && obj.nodeType === 1 && typeof obj.style === "object" && typeof obj.ownerDocument === "object" ); } }