import React, { useRef, useEffect, useState } from 'react'; import qrcodegen from '@digigov/ui/app/QrCodeViewer/qrcodegen'; import { QRPropsCanvas } from '@digigov/ui/app/QrCodeViewer/types'; import { generatePath, excavateModules, getImageSettings, MARGIN_SIZE, } from '@digigov/ui/app/QrCodeViewer/utils'; const DEFAULT_SIZE = 128; const DEFAULT_LEVEL = 'L'; const DEFAULT_BGCOLOR = '#FFFFFF'; const DEFAULT_FGCOLOR = '#000000'; const DEFAULT_INCLUDEMARGIN = false; const SUPPORTS_PATH2D = (function () { try { new Path2D().addPath(new Path2D()); } catch { return false; } return true; })(); const ERROR_LEVEL_MAP: Record = { L: qrcodegen.QrCode.Ecc.LOW, M: qrcodegen.QrCode.Ecc.MEDIUM, Q: qrcodegen.QrCode.Ecc.QUARTILE, H: qrcodegen.QrCode.Ecc.HIGH, }; const QrCodeViewerCanvas = React.forwardRef(function QrCodeViewerCanvas( props: QRPropsCanvas, ref: any ) { const { value, size = DEFAULT_SIZE, level = DEFAULT_LEVEL, bgColor = DEFAULT_BGCOLOR, fgColor = DEFAULT_FGCOLOR, includeMargin = DEFAULT_INCLUDEMARGIN, style, imageSettings, ...otherProps } = props; const imgSrc = imageSettings?.src; const _canvasRef = useRef(null); const _canvas = ref || _canvasRef; const _image = useRef(null); // We're just using this state to trigger rerenders when images load. We // Don't actually read the value anywhere. A smarter use of useEffect would // depend on this value. const [isImgLoaded, setIsImageLoaded] = useState(false); useEffect(() => { // Always update the canvas. It's cheap enough and we want to be correct // with the current state. if (_canvas.current !== null) { const canvas = _canvas.current; const ctx = canvas.getContext('2d'); if (!ctx) { return; } let cells = qrcodegen.QrCode.encodeText( value, ERROR_LEVEL_MAP[level] ).getModules(); const margin = includeMargin ? MARGIN_SIZE : 0; const numCells = cells.length + margin * 2; const calculatedImageSettings = getImageSettings( cells, size, includeMargin, imageSettings ); const image = _image.current; const haveImageToRender = calculatedImageSettings != null && image !== null && image.complete && image.naturalHeight !== 0 && image.naturalWidth !== 0; if (haveImageToRender && calculatedImageSettings) { if (calculatedImageSettings.excavation != null) { cells = excavateModules(cells, calculatedImageSettings.excavation); } } // We're going to scale this so that the number of drawable units // matches the number of cells. This avoids rounding issues, but does // result in some potentially unwanted single pixel issues between // blocks, only in environments that don't support Path2D. const pixelRatio = window.devicePixelRatio || 1; canvas.height = canvas.width = size * pixelRatio; const scale = (size / numCells) * pixelRatio; ctx.scale(scale, scale); // Draw solid background, only paint dark modules. ctx.fillStyle = bgColor; ctx.fillRect(0, 0, numCells, numCells); ctx.fillStyle = fgColor; if (SUPPORTS_PATH2D) { // $FlowFixMe: Path2D c'tor doesn't support args yet. ctx.fill(new Path2D(generatePath(cells, margin))); } else { cells.forEach(function (row, rdx) { row.forEach(function (cell, cdx) { if (cell) { ctx.fillRect(cdx + margin, rdx + margin, 1, 1); } }); }); } if (haveImageToRender && calculatedImageSettings) { if (image instanceof HTMLImageElement) { ctx.drawImage( image, calculatedImageSettings.x + margin, calculatedImageSettings.y + margin, calculatedImageSettings.w, calculatedImageSettings.h ); } } } }); // Ensure we mark image loaded as false here so we trigger updating the // canvas in our other effect. useEffect(() => { setIsImageLoaded(false); }, [imgSrc, isImgLoaded]); const canvasStyle = { height: size, width: size, ...style }; let img; if (imgSrc != null) { img = ( { setIsImageLoaded(true); }} ref={_image} /> ); } return ( <> {img} ); }); export interface QrCodeViewerProps extends QRPropsCanvas {} export const QrCodeViewer = React.forwardRef(function QrCodeViewer( props: QrCodeViewerProps, ref ) { return ; }); export default QrCodeViewer;