Source: Output/QRCanvas.js

/**
 * @created      11.07.2022
 * @author       smiley <smiley@chillerlan.net>
 * @copyright    2022 smiley
 * @license      MIT
 */

import QROutputAbstract from './QROutputAbstract.js';
import QRCodeOutputException from './QRCodeOutputException.js';

/**
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
 * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas
 */
export default class QRCanvas extends QROutputAbstract{

	/**
	 * @inheritDoc
	 */
	mimeType = 'image/png';

	/**
	 * @type {HTMLCanvasElement}
	 * @protected
	 */
	canvas;

	/**
	 * @type {CanvasRenderingContext2D}
	 * @protected
	 */
	context;

	/**
	 * @inheritDoc
	 */
	static moduleValueIsValid($value){

		if(typeof $value !== 'string'){
			return false;
		}

		$value = $value.trim();

		// hex notation
		// #rgb(a)
		// #rrggbb(aa)
		if($value.match(/^#([\da-f]{3}){1,2}$|^#([\da-f]{4}){1,2}$/i)){
			return true;
		}

		// css: hsla/rgba(...values)
		if($value.match(/^(hsla?|rgba?)\([\d .,%\/]+\)$/i)){
			return true;
		}

		// predefined css color
		if($value.match(/^[a-z]+$/i)){
			return true;
		}

		return false;
	}

	/**
	 * @inheritDoc
	 */
	prepareModuleValue($value){
		return $value.trim();
	}

	/**
	 * @inheritDoc
	 */
	getDefaultModuleValue($isDark){
		return $isDark ? '#000' : '#fff';
	}

	/**
	 * @inheritDoc
	 */
	getOutputDimensions(){
		return [this.length, this.length];
	}

	/**
	 * @param {string} $data (ignored)
	 * @param {string} $mime (ignored)
	 * @returns {string}
	 * @protected
	 */
	toBase64DataURI($data, $mime){
		$mime = this.options.canvasMimeType.trim();

		if($mime === ''){
			$mime = this.mimeType;
		}

		return this.canvas.toDataURL($mime, this.options.canvasImageQuality)
	}

	/**
	 * @inheritDoc
	 *
	 * @returns {HTMLCanvasElement|string|*}
	 * @throws {QRCodeOutputException}
	 */
	dump($file = null){
		this.canvas = (this.options.canvasElement || document.createElement('canvas'));

		// @todo: test if instance check also works with nodejs canvas modules etc.
		if(!this.canvas || !(this.canvas instanceof HTMLCanvasElement) || (typeof this.canvas.getContext !== 'function')){
			throw new QRCodeOutputException('invalid canvas element');
		}

		this.drawImage();

		if(this.options.returnAsDomElement){
			return this.canvas;
		}

		let base64DataURI = this.toBase64DataURI();
		let rawImage      = atob(base64DataURI.split(',')[1]);

		this.saveToFile(rawImage, $file);

		if(this.options.outputBase64){
			return base64DataURI;
		}

		return rawImage;
	}

	/**
	 * @returns {void}
	 * @protected
	 */
	drawImage(){
		this.canvas.width  = this.length;
		this.canvas.height = this.length;
		this.context       = this.canvas.getContext('2d', {alpha: this.options.imageTransparent})

		if(this.options.bgcolor && this.constructor.moduleValueIsValid(this.options.bgcolor)){
			this.context.fillStyle = this.options.bgcolor;
			this.context.fillRect(0, 0, this.length, this.length);
		}

		for(let $y = 0; $y < this.moduleCount; $y++){
			for(let $x = 0; $x < this.moduleCount; $x++){
				this.module($x, $y, this.matrix.get($x, $y))
			}
		}

	}

	/**
	 * @returns {void}
	 * @protected
	 */
	module($x, $y, $M_TYPE){

		if(!this.options.drawLightModules && !this.matrix.check($x, $y)){
			return;
		}

		this.context.fillStyle = this.getModuleValue($M_TYPE);

		if(this.options.drawCircularModules && !this.matrix.checkTypeIn($x, $y, this.options.keepAsSquare)){
			this.context.beginPath();

			this.context.arc(
				($x + 0.5) * this.scale,
				($y + 0.5) * this.scale,
				(this.options.circleRadius * this.scale),
				0,
				2 * Math.PI
			)

			this.context.fill();

			return;
		}

		this.context.fillRect($x * this.scale, $y * this.scale, this.scale, this.scale);
	}

}