3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

QRコード認識Reactコンポーネント

Last updated at Posted at 2021-06-16

画面イメージ(認識箇所を赤枠で囲います)

qr-reader.png

サンプルページ(github pages)

前書き

ブラウザでQRコードの認識できるのか?と調べたところ、jsqrというライブラリで簡単に出来ることがわかりました。
そのReact版です。

技術的な特徴

  • カメラ動画を<video>タグでブラウザに表示。タイマーで定期的に画像化してQR認識ライブラリjsqrに引き渡します。

  • DOM(videoタグ)を直接操作する必要があるためuseRef()フックを利用

  • setInterval()で定期的にQRコード認識ライブラリを呼び出します

  • 認識時にcallBack関数で通知し、停止するかそのまま継続するか選ぶことができます。

主な機能

  • props:<video>タグのサイズ、停止/再開(pause)、認識時枠表示(showQRFrame)を指定します
export type QRReaderProps = {
  width?: number,
  height?: number,
  pause?: boolean,
  showQRFrame?: boolean,
  timerInterval?: number,
  onRecognizeCode?: (e: QRCode) => boolean,
}

  • 認識したタイミングで、利用側にcallback(onRecognizeCode())を行います。
  const onRecognizeCode = (e: QRCode) => {
    setCode(e.data);
    if (stopOnRecognize) {
      setQRParam( e => { return {...e, pause: true}; });
    }
  }

QR認識コンポーネント概要

一部を抜粋し、説明用に書き換えてあります。

  1. <video>タグを配置し、ReactからDOMを直接操作するためuseRef()で参照します。
  const video = useRef(null as HTMLVideoElement);
  <video ref={video}></video>

実際のソースでは、styled-componentを利用しているため、<VideoArea>になっています。

  1. カメラの入力ストリームを取得し、videoタグに接続します。
  const constraints = { 
    audio: false, 
    video: {
      facingMode: 'environment', 
      width, 
      height, 
  }};

  // useRef()を利用する場合、video.currentで参照します
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
  video.current.srcObject = stream;
  video.current.play();
  1. video動画を画像に変換するために必要なcanvas1を用意します。サイズはvideo側と合わせます。
  const canvas = new OffscreenCanvas(width, height);
  const context = canvas.getContext('2d');
  1. タイマー(setInterval())を利用して定期的にQRコード認識処理を呼び出します。
  timerId.current = setInterval(() => {
    // video動画から画像に変換
    context.drawImage(video.current, 0, 0, width, height);
    const imageData = context.getImageData(0, 0, width, height);
    // QR認識ライブラリに引き渡す
    const qr = jsqr(imageData.data, imageData.width, imageData.height);
  }
  1. qrがnullでなければコードが認識されています。
  • qr.dataに読み取った値が入っています

  • qr.locationには、QRコードの位置が入っています。認識したコードを赤枠で囲うためdrawRect()を呼び出して枠をオーバーレイ表示しています

  • props.onRecognizeCodeがnullでなければ、呼び出し元にコールバックします

  if (qr) {
    console.log(qr.data);
    if (props.showQRFrame) {
      drawRect(qr.location.topLeftCorner, qr.location.bottomRightCorner);
    }
    if (props.onRecognizeCode) props.onRecognizeCode(qr);               
  }

コンポーネント全体

import React, { useState, useEffect, useRef } from 'react';
import styled from 'styled-components';
import jsqr, { QRCode } from 'jsqr';
export type { QRCode } from 'jsqr';

export type QRReaderProps = {
  width?: number,
  height?: number,
  pause?: boolean,
  showQRFrame?: boolean,
  timerInterval?: number,
  gecognizeCallback?: (e: QRCode) => void,
}

type Point = {
  x: number;
  y: number;
}

type OverlayPosition = {
  top: number,
  left: number,
  width: number,
  height: number,
}

const RelativeWrapperDiv = styled.div<QRReaderProps>`
  position: relative;
  width : ${(props) => props.width}px;
  height: ${(props) => props.height}px;
`;

const VideoArea = styled.video`
  position: absolute; 
  z-index : -100;
`;

const OverlayDiv = styled.div<OverlayPosition>`
  position: absolute; 
  border: 1px solid #F00;
  top   : ${(props) => props.top}px;
  left  : ${(props) => props.left}px;
  width : ${(props) => props.width}px;
  height: ${(props) => props.height}px;
`;


const QRReader: React.FC<QRReaderProps> = (props) => {
  const [overlay, setOverlay] = useState({ top:0, left: 0, width: 0, height: 0 });  
  const video = useRef(null as HTMLVideoElement);
  const timerId = useRef(null);

  const drawRect = (topLeft: Point, bottomRight: Point) => {
    setOverlay({
      top: topLeft.y < bottomRight.y ? topLeft.y : bottomRight.y,
      left: topLeft.x < bottomRight.x ? topLeft.x :bottomRight.x,
      width: Math.abs(bottomRight.x - topLeft.x),
      height: Math.abs(bottomRight.y - topLeft.y),
    });
  };

  useEffect(() => {
    (async() => {
      if (props.pause) {
        video.current.pause();
        clearInterval(timerId.current);
        timerId.current = null;
        return;
      }

      const { width, height } = props;

      const constraints = { 
        audio: false, 
        video: {
          facingMode: 'environment', 
          width, 
          height, 
      }};
    
      const stream = await navigator.mediaDevices.getUserMedia(constraints);
      video.current.srcObject = stream;
      video.current.play();
  
      const canvas = new OffscreenCanvas(width, height);
      const context = canvas.getContext('2d');

      if (!timerId.current) {
        timerId.current = setInterval(() => {
          context.drawImage(video.current, 0, 0, width, height);
          const imageData = context.getImageData(0, 0, width, height);
          const qr = jsqr(imageData.data, imageData.width, imageData.height);
          if (qr) {
            console.log(qr.data);
            if (props.showQRFrame) {
              drawRect(qr.location.topLeftCorner, qr.location.bottomRightCorner);
            }
            if (props.gecognizeCallback) props.gecognizeCallback(qr);               
          }
        }, props.timerInterval);
      }
      return () => clearInterval(timerId.current);
    })();
  }, [props]);



  return (    
    <RelativeWrapperDiv {...props}>
      <VideoArea ref={video}></VideoArea>
      <OverlayDiv {...overlay}></OverlayDiv>
    </RelativeWrapperDiv>    
  );
}

// propsのデフォルト値を設定
QRReader.defaultProps = {
  width: 500,
  height: 500,
  pause: false,
  showQRFrame: true,
  timerInterval: 300,
};

export default QRReader;

利用方法

  • コンポーネントのサイズと(width, height)と、読み取り状態(pause)に初期値をセットします。
  const [qrParam, setQRParam] = useState({
    width: 500,
    height: 500,
    pause: true,
  });
  • onRecognizeCode()で認識されたQRコードを取得し、読み取りを停止します。
  const onRecognizeCode = (e: QRCode) => {
    setCode(e.data);
    if (stopOnRecognize) {
      setQRParam( e => { return {...e, pause: true}; });
    }
  }`
  • toggleVideoStream()で停止と再開を切り替えます
  const toggleVideoStream = () => {
    setQRParam( e => { return {...e, pause: !e.pause}; });
  }
import React, { useState } from 'react';
import QRReader, { QRCode } from './QRReader';

function App() {
  const [stopOnRecognize, setStopOnRecognize] = React.useState(true);
  const [qrParam, setQRParam] = useState({
    width: 500,
    height: 500,
    pause: true,
  });

  const [code, setCode] = useState('');

  const onRecognizeCode = (e: QRCode) => {
    setCode(e.data);
    if (stopOnRecognize) {
      setQRParam( e => { return {...e, pause: true}; });
    }
  }

  const toggleVideoStream = () => {
    setQRParam( e => { return {...e, pause: !e.pause}; });
  }

  return (
    <div className="App">
      <QRReader {...qrParam} onRecognizeCode={onRecognizeCode} />
      <div>
        <label>
          <input type="radio" name="rdo" value="0" onChange={(e) => setStopOnRecognize(e.target.value === "0")} checked={stopOnRecognize} />認識時に自動停        </label>
        <label>
          <input type="radio" name="rdo" value="1" onChange={(e) => setStopOnRecognize(e.target.value === "0")} checked={!stopOnRecognize} />認識時も処理継        </label>
        
        <button onClick={toggleVideoStream}>{(qrParam.pause? '再開': '停止')}</button>
        <p>QRコード{code}</p>
      </div>

    </div>
  );
}

export default App;

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?