From fcf61dcee32ddac7777bb1266614703534e2e671 Mon Sep 17 00:00:00 2001 From: PastLeo Date: Fri, 1 Feb 2019 16:41:35 +0800 Subject: [PATCH 1/4] able to add img on canvas/svg and download add `img` prop: { src, left, top, width, height } add `genCanvas()`, `genCanvasDataURL()` and `download()` to provide download feature --- src/index.js | 223 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 173 insertions(+), 50 deletions(-) diff --git a/src/index.js b/src/index.js index a147898..e2eac48 100644 --- a/src/index.js +++ b/src/index.js @@ -42,6 +42,21 @@ function convertStr(str: string): string { return out; } +type ImgProps = { + src: string, + left: number, + top: number, + width: number, + height: number, +} + +const DEFAULT_IMG_PROPS = { + left: 50, + top: 50, + width: 10, + height: 10, +} + type QRProps = { value: string, size: number, @@ -50,6 +65,7 @@ type QRProps = { fgColor: string, style?: ?Object, includeMargin: boolean, + img: ?ImgProps, }; const DEFAULT_PROPS = { @@ -71,6 +87,80 @@ const PROP_TYPES = { const MARGIN_SIZE = 4; +function drawQrOnCanvas( + qrcode: Object, + canvas: HTMLCanvasElement, + size: number, + margin: number, + bgColor: string, + fgColor: string, + imgProps: ?ImgProps, +): Promise { + return new Promise((resolve, reject) => { + const ctx = canvas.getContext('2d'); + if (!ctx) { + return reject(new Error('canvas.getContext("2d") failed')); + } + + const cells = qrcode.modules; + if (cells === null) { + return reject(new Error('qrcode.modules is null')); + } + + const numCells = cells.length + margin * 2; + + // 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); + } + }); + }); + } + + // reset scale to allow outside manipulation + ctx.scale(1 / scale, 1 / scale); + + if (imgProps && imgProps.src) { + const {src, left, top, width, height} = + {...DEFAULT_IMG_PROPS, ...(imgProps || {})}; + const img = new Image(); + img.src = src; + + img.addEventListener('error', reject); + img.addEventListener('load', () => { + const dWidth = canvas.width * width / 100; + const dHeight = canvas.height * height / 100; + const dx = canvas.width * left / 100 - dWidth / 2 + const dy = canvas.height * top / 100 - dHeight / 2 + ctx.drawImage(img, dx, dy, dWidth, dHeight); + + resolve(canvas); + }); + } else { + resolve(canvas); + } + }); +} + function generatePath(modules: [[boolean]], margin: number = 0): string { const ops = []; modules.forEach(function(row, y) { @@ -134,7 +224,7 @@ class QRCodeCanvas extends React.PureComponent { } update() { - const {value, size, level, bgColor, fgColor, includeMargin} = this.props; + const {value, size, level, bgColor, fgColor, includeMargin, img} = this.props; // We'll use type===-1 to force QRCode to automatically pick the best type const qrcode = new QRCodeImpl(-1, ErrorCorrectLevel[level]); @@ -142,48 +232,15 @@ class QRCodeCanvas extends React.PureComponent { qrcode.make(); if (this._canvas != null) { - const canvas = this._canvas; - - const ctx = canvas.getContext('2d'); - if (!ctx) { - return; - } - - const cells = qrcode.modules; - if (cells === null) { - return; - } - - const margin = includeMargin ? MARGIN_SIZE : 0; - - const numCells = cells.length + margin * 2; - - // 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); - } - }); - }); - } + drawQrOnCanvas( + qrcode, + this._canvas, + size, + includeMargin ? MARGIN_SIZE : 0, + bgColor, + fgColor, + img, + ) } } @@ -217,6 +274,24 @@ class QRCodeSVG extends React.PureComponent { static defaultProps = DEFAULT_PROPS; static propTypes = PROP_TYPES; + renderImg(imgProps: ?ImgProps) { + if (imgProps && imgProps.src) { + const {src, left, top, width, height} = + {...DEFAULT_IMG_PROPS, ...(imgProps || {})}; + const [x, y, w, h] = + [left - width / 2, top - height / 2, width, height].map(v => `${v}%`); + + return ( + + ); + } else { + return null; + } + } + render() { const { value, @@ -225,6 +300,7 @@ class QRCodeSVG extends React.PureComponent { bgColor, fgColor, includeMargin, + img, ...otherProps } = this.props; @@ -259,18 +335,65 @@ class QRCodeSVG extends React.PureComponent { {...otherProps}> + {this.renderImg(img)} ); } } -type RootProps = QRProps & {renderAs: 'svg' | 'canvas'}; -const QRCode = (props: RootProps): React.Node => { - const {renderAs, ...otherProps} = props; - const Component = renderAs === 'svg' ? QRCodeSVG : QRCodeCanvas; - return ; -}; +const DEFAULT_DATA_URL_TYPE = 'image/png'; +const DEFAULT_DOWNLOAD_FILENAME = 'QRCode.png'; + +type RootProps = QRProps & { renderAs: 'svg' | 'canvas' }; +class QRCode extends React.Component { + static defaultProps = {renderAs: 'canvas', ...DEFAULT_PROPS}; + + genCanvas(overwritingProps: QRProps): Promise { + const canvas = document.createElement('canvas'); + const {value, size, level, bgColor, fgColor, includeMargin, img} = + {...this.props, ...(overwritingProps || {})}; + + // We'll use type===-1 to force QRCode to automatically pick the best type + const qrcode = new QRCodeImpl(-1, ErrorCorrectLevel[level]); + qrcode.addData(convertStr(value)); + qrcode.make(); -QRCode.defaultProps = {renderAs: 'canvas', ...DEFAULT_PROPS}; + return drawQrOnCanvas( + qrcode, + canvas, + size, + includeMargin ? MARGIN_SIZE : 0, + bgColor, + fgColor, + img, + ); + } + + genCanvasDataURL(type: string = DEFAULT_DATA_URL_TYPE, overwritingProps: QRProps): Promise { + return this.genCanvas(overwritingProps) + .then(canvas => canvas.toDataURL(type)); + } + + download(filename: string = DEFAULT_DOWNLOAD_FILENAME, type: string = DEFAULT_DATA_URL_TYPE, overwritingProps: QRProps) { + this.genCanvasDataURL(type, overwritingProps) + .then(dataUrl => { + const downloadLink = document.createElement('a'); + + downloadLink.setAttribute( + 'href', + dataUrl.replace(type, 'image/octet-stream') + ); + downloadLink.setAttribute('download', filename); + + downloadLink.click(); + }); + } + + render(): React.Node { + const {renderAs, ...otherProps} = this.props; + const Component = renderAs === 'svg' ? QRCodeSVG : QRCodeCanvas; + return ; + } +} module.exports = QRCode; From cfeb4671df8a07118a3da7ae0f798b77531de268 Mon Sep 17 00:00:00 2001 From: PastLeo Date: Fri, 1 Feb 2019 16:47:07 +0800 Subject: [PATCH 2/4] update example --- examples/demo.js | 99 ++++++++++++++++++++++++++++++++++++++++++++-- examples/logo.png | Bin 0 -> 1240 bytes 2 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 examples/logo.png diff --git a/examples/demo.js b/examples/demo.js index a942db3..f87c283 100644 --- a/examples/demo.js +++ b/examples/demo.js @@ -4,7 +4,6 @@ var QRCode = require('..'); var React = require('react'); var ReactDOM = require('react-dom'); -// TODO: live update demo class Demo extends React.Component { state = { value: 'http://picturesofpeoplescanningqrcodes.tumblr.com/', @@ -14,9 +13,27 @@ class Demo extends React.Component { level: 'L', renderAs: 'svg', includeMargin: false, + withImg: false, + img: { + src: './logo.png', + top: 50, + left: 50, + width: 10, + height: 10, + } }; + downloadQRCode = () => { + this.qrcode.download('QR Code.png'); + } + + setImgState(state) { + this.setState({img: {...this.state.img, ...state}}); + } + render() { + var imgCode = this.state.withImg ? + ` img={${JSON.stringify(this.state.img)}}\n` : '' var code = ``; + ref={ref => this.qrcode = ref} +${imgCode}/>` return (
@@ -100,6 +118,74 @@ class Demo extends React.Component {
+
+ +
+
+
+ +
+
+ +
+
+ + +
+ + +
+
+