gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: fix #7535: fix qr implementat


From: gnunet
Subject: [taler-wallet-core] branch master updated: fix #7535: fix qr implementation
Date: Tue, 31 Jan 2023 14:21:17 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 9d9a88af0 fix #7535: fix qr implementation
9d9a88af0 is described below

commit 9d9a88af010ac39f026299ebccea3e1164e5242e
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Jan 31 10:21:08 2023 -0300

    fix #7535: fix qr implementation
---
 packages/taler-wallet-webextension/package.json    |   2 +-
 .../src/wallet/QrReader.tsx                        | 396 ++++++++++++++++-----
 pnpm-lock.yaml                                     |  14 +-
 3 files changed, 310 insertions(+), 102 deletions(-)

diff --git a/packages/taler-wallet-webextension/package.json 
b/packages/taler-wallet-webextension/package.json
index 076e43dc1..226ea757e 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -25,9 +25,9 @@
     "@gnu-taler/taler-wallet-core": "workspace:*",
     "date-fns": "^2.29.2",
     "history": "4.10.1",
+    "jsqr": "^1.4.0",
     "preact": "10.11.3",
     "preact-router": "3.2.1",
-    "qr-scanner": "^1.4.1",
     "qrcode-generator": "^1.4.4",
     "tslib": "^2.4.0"
   },
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx 
b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
index 467f8bb7c..c1972823a 100644
--- a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
@@ -14,17 +14,25 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
+import {
+  classifyTalerUri,
+  TalerUriType,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import { styled } from "@linaria/react";
+import { css } from "@linaria/core";
 import { Fragment, h, VNode } from "preact";
-import { Ref, useEffect, useRef, useState } from "preact/hooks";
-import QrScanner from "qr-scanner";
+import { Ref, useEffect, useMemo, useRef, useState } from "preact/hooks";
 import { useTranslationContext } from "../context/translation.js";
 import { Alert } from "../mui/Alert.js";
 import { Button } from "../mui/Button.js";
 import { TextField } from "../mui/TextField.js";
+import jsQR, * as pr from "jsqr";
+import { InputFile } from "../mui/InputFile.js";
+import { Grid } from "../mui/Grid.js";
+import { notDeepEqual } from "assert";
 
-const QrVideo = styled.video`
+const QrCanvas = css`
   width: 80%;
   margin-left: auto;
   margin-right: auto;
@@ -32,6 +40,8 @@ const QrVideo = styled.video`
   background-color: black;
 `;
 
+const LINE_COLOR = "#FF3B58";
+
 const Container = styled.div`
   display: flex;
   flex-direction: column;
@@ -44,111 +54,303 @@ interface Props {
   onDetected: (url: string) => void;
 }
 
+type XY = { x: number; y: number };
+
+function drawLine(
+  canvas: CanvasRenderingContext2D,
+  begin: XY,
+  end: XY,
+  color: string,
+) {
+  canvas.beginPath();
+  canvas.moveTo(begin.x, begin.y);
+  canvas.lineTo(end.x, end.y);
+  canvas.lineWidth = 4;
+  canvas.strokeStyle = color;
+  canvas.stroke();
+}
+
+function drawBox(context: CanvasRenderingContext2D, code: pr.QRCode) {
+  drawLine(
+    context,
+    code.location.topLeftCorner,
+    code.location.topRightCorner,
+    LINE_COLOR,
+  );
+  drawLine(
+    context,
+    code.location.topRightCorner,
+    code.location.bottomRightCorner,
+    LINE_COLOR,
+  );
+  drawLine(
+    context,
+    code.location.bottomRightCorner,
+    code.location.bottomLeftCorner,
+    LINE_COLOR,
+  );
+  drawLine(
+    context,
+    code.location.bottomLeftCorner,
+    code.location.topLeftCorner,
+    LINE_COLOR,
+  );
+}
+
+const SCAN_PER_SECONDS = 3;
+const TIME_BETWEEN_FRAMES = 1000 / SCAN_PER_SECONDS;
+
+async function delay(ms: number) {
+  return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function drawIntoCanvasAndGetQR(
+  tag: HTMLVideoElement | HTMLImageElement,
+  canvas: HTMLCanvasElement,
+): string | undefined {
+  const context = canvas.getContext("2d");
+  if (!context) {
+    throw Error("no 2d canvas context");
+  }
+  context.clearRect(0, 0, canvas.width, canvas.height);
+  context.drawImage(tag, 0, 0, canvas.width, canvas.height);
+  const imgData = context.getImageData(0, 0, canvas.width, canvas.height);
+  const code = jsQR(imgData.data, canvas.width, canvas.height, {
+    inversionAttempts: "attemptBoth",
+  });
+  if (code) {
+    drawBox(context, code);
+    return code.data;
+  }
+  return undefined;
+}
+
+async function readNextFrame(
+  video: HTMLVideoElement,
+  canvas: HTMLCanvasElement,
+): Promise<string | undefined> {
+  const requestFrame =
+    "requestVideoFrameCallback" in video
+      ? video.requestVideoFrameCallback.bind(video)
+      : requestAnimationFrame;
+
+  return new Promise<string | undefined>((ok, bad) => {
+    requestFrame(() => {
+      try {
+        const code = drawIntoCanvasAndGetQR(video, canvas);
+        ok(code);
+      } catch (error) {
+        bad(error);
+      }
+    });
+  });
+}
+
+async function createCanvasFromVideo(
+  video: HTMLVideoElement,
+  canvas: HTMLCanvasElement,
+): Promise<string> {
+  const context = canvas.getContext("2d", {
+    willReadFrequently: true,
+  });
+  if (!context) {
+    throw Error("no 2d canvas context");
+  }
+  canvas.width = video.videoWidth;
+  canvas.height = video.videoHeight;
+
+  let last = Date.now();
+
+  let found: string | undefined = undefined;
+  while (!found) {
+    const timeSinceLast = Date.now() - last;
+    if (timeSinceLast < TIME_BETWEEN_FRAMES) {
+      await delay(TIME_BETWEEN_FRAMES - timeSinceLast);
+    }
+    last = Date.now();
+    found = await readNextFrame(video, canvas);
+  }
+  video.pause();
+  return found;
+}
+
+async function createCanvasFromFile(
+  source: string,
+  canvas: HTMLCanvasElement,
+): Promise<string | undefined> {
+  const img = new Image(300, 300);
+  img.src = source;
+  canvas.width = img.width;
+  canvas.height = img.height;
+  return new Promise<string | undefined>((ok, bad) => {
+    img.addEventListener("load", (e) => {
+      try {
+        const code = drawIntoCanvasAndGetQR(img, canvas);
+        ok(code);
+      } catch (error) {
+        bad(error);
+      }
+    });
+  });
+}
+
+async function waitUntilReady(video: HTMLVideoElement): Promise<void> {
+  return new Promise((ok, bad) => {
+    if (video.readyState === video.HAVE_ENOUGH_DATA) {
+      return ok();
+    }
+    setTimeout(waitUntilReady, 100);
+  });
+}
+
 export function QrReaderPage({ onDetected }: Props): VNode {
   const videoRef = useRef<HTMLVideoElement>(null);
-  // const imageRef = useRef<HTMLImageElement>(null);
-  const qrScanner = useRef<QrScanner | null>(null);
-  const [value, onChange] = useState("");
-  const [active, setActive] = useState(false);
+  const canvasRef = useRef<HTMLCanvasElement>(null);
+  const [error, setError] = useState<TranslatedString | undefined>();
+  const [value, setValue] = useState("");
+  const [show, setShow] = useState<"canvas" | "video" | "nothing">("nothing");
+
   const { i18n } = useTranslationContext();
 
-  function start(): void {
-    qrScanner.current!.start();
-    onChange("");
-    setActive(true);
-  }
-  function stop(): void {
-    qrScanner.current!.stop();
-    setActive(false);
+  function onChange(str: string) {
+    if (!!str) {
+      if (!str.startsWith("taler://")) {
+        setError(
+          i18n.str`URI is not valid. Taler URI should start with "taler://"`,
+        );
+      } else if (classifyTalerUri(str) === TalerUriType.Unknown) {
+        setError(i18n.str`Unknown type of Taler URI`);
+      } else {
+        setError(undefined);
+      }
+    } else {
+      setError(undefined);
+    }
+    setValue(str);
   }
 
-  function check(v: string) {
-    return (
-      v.startsWith("taler://") && classifyTalerUri(v) !== TalerUriType.Unknown
-    );
+  async function startVideo() {
+    if (!videoRef.current || !canvasRef.current) {
+      return;
+    }
+    const video = videoRef.current;
+    if (!video || !video.played) return;
+    const stream = await navigator.mediaDevices.getUserMedia({
+      video: { facingMode: "environment" },
+      audio: false,
+    });
+    setShow("video");
+    setError(undefined);
+    video.srcObject = stream;
+    await video.play();
+    await waitUntilReady(video);
+    try {
+      const code = await createCanvasFromVideo(video, canvasRef.current);
+      if (code) {
+        onChange(code);
+        setShow("canvas");
+      }
+      stream.getTracks().forEach((e) => {
+        e.stop();
+      });
+    } catch (error) {
+      setError(i18n.str`something unexpected happen: ${error}`);
+    }
   }
 
-  useEffect(() => {
-    if (!videoRef.current) {
-      console.log("vide was not ready");
+  async function onFileRead(fileContent: string) {
+    if (!canvasRef.current) {
       return;
     }
-    const elem = videoRef.current;
-    setTimeout(() => {
-      qrScanner.current = new QrScanner(
-        elem,
-        ({ data, cornerPoints }) => {
-          if (check(data)) {
-            onDetected(data);
-            return;
-          }
-          onChange(data);
-          stop();
-        },
-        {
-          maxScansPerSecond: 5, //default 25
-          highlightScanRegion: true,
-        },
-      );
-      start();
-    }, 1);
-    return () => {
-      qrScanner.current?.destroy();
-    };
-  }, []);
-
-  const isValid = check(value);
+    setShow("nothing");
+    setError(undefined);
+    try {
+      const code = await createCanvasFromFile(fileContent, canvasRef.current);
+      if (code) {
+        onChange(code);
+        setShow("canvas");
+      } else {
+        setError(i18n.str`Could not found a QR code in the file`);
+      }
+    } catch (error) {
+      setError(i18n.str`something unexpected happen: ${error}`);
+    }
+  }
+
+  const active = value === "";
   return (
     <Container>
-      {/* <InputFile onChange={(f) => scanImage(imageRef, f)}>
-        Read QR from file
-      </InputFile>
-      <div ref={imageRef} /> */}
-      <h1>
-        <i18n.Translate>
-          Scan a QR code or enter taler:// URI below
-        </i18n.Translate>
-      </h1>
-      <QrVideo ref={videoRef} />
-      <TextField
-        label="Taler URI"
-        variant="standard"
-        fullWidth
-        value={value}
-        onChange={onChange}
-      />
-      {isValid && (
-        <Button variant="contained" onClick={async () => onDetected(value)}>
-          <i18n.Translate>Open</i18n.Translate>
-        </Button>
-      )}
-      {!active && !isValid && (
-        <Fragment>
-          <Alert severity="error">
-            <i18n.Translate>
-              URI is not valid. Taler URI should start with `taler://`
-            </i18n.Translate>
-          </Alert>
-          <Button variant="contained" onClick={async () => start()}>
-            <i18n.Translate>Try another</i18n.Translate>
-          </Button>
-        </Fragment>
-      )}
+      <section>
+        <h1>
+          <i18n.Translate>
+            Scan a QR code or enter taler:// URI below
+          </i18n.Translate>
+        </h1>
+
+        <p>
+          <TextField
+            label="Taler URI"
+            variant="standard"
+            fullWidth
+            value={value}
+            onChange={onChange}
+          />
+        </p>
+        <Grid container justifyContent="space-around" columns={2}>
+          <Grid item xs={2}>
+            <p>{error && <Alert severity="error">{error}</Alert>}</p>
+          </Grid>
+          <Grid item xs={1}>
+            {!active && (
+              <Button
+                variant="contained"
+                onClick={async () => {
+                  setShow("nothing");
+                  onChange("");
+                }}
+                color="error"
+              >
+                <i18n.Translate>Clear</i18n.Translate>
+              </Button>
+            )}
+          </Grid>
+          <Grid item xs={1}>
+            {value && (
+              <Button
+                disabled={!!error}
+                variant="contained"
+                color="success"
+                onClick={async () => onDetected(value)}
+              >
+                <i18n.Translate>Open</i18n.Translate>
+              </Button>
+            )}
+          </Grid>
+          <Grid item xs={1}>
+            <InputFile onChange={onFileRead}>Read QR from file</InputFile>
+          </Grid>
+          <Grid item xs={1}>
+            <p>
+              <Button variant="contained" onClick={startVideo}>
+                Use Camera
+              </Button>
+            </p>
+          </Grid>
+        </Grid>
+      </section>
+      <div>
+        <video
+          ref={videoRef}
+          style={{ display: show === "video" ? "unset" : "none" }}
+          playsInline={true}
+        />
+        <canvas
+          id="este"
+          class={QrCanvas}
+          ref={canvasRef}
+          style={{ display: show === "canvas" ? "unset  " : "none" }}
+        />
+      </div>
     </Container>
   );
 }
-
-async function scanImage(
-  imageRef: Ref<HTMLImageElement>,
-  image: string,
-): Promise<void> {
-  const imageEl = new Image();
-  imageEl.src = image;
-  imageEl.width = 200;
-  imageRef.current!.appendChild(imageEl);
-  QrScanner.scanImage(image, {
-    alsoTryWithoutScanRegion: true,
-  })
-    .then((result) => console.log(result))
-    .catch((error) => console.log(error || "No QR code found."));
-}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ebd352a8c..99c71d823 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -574,6 +574,7 @@ importers:
       date-fns: ^2.29.2
       esbuild: ^0.15.13
       history: 4.10.1
+      jsqr: ^1.4.0
       mocha: ^9.2.0
       nyc: ^15.1.0
       polished: ^4.1.4
@@ -581,7 +582,7 @@ importers:
       preact-cli: ^3.3.5
       preact-render-to-string: ^5.1.19
       preact-router: 3.2.1
-      qr-scanner: ^1.4.1
+      qr-scanner: 1.4.2
       qrcode-generator: ^1.4.4
       rimraf: ^3.0.2
       tslib: ^2.4.0
@@ -591,9 +592,10 @@ importers:
       '@gnu-taler/taler-wallet-core': link:../taler-wallet-core
       date-fns: 2.29.3
       history: 4.10.1
+      jsqr: 1.4.0
       preact: 10.11.3
       preact-router: 3.2.1_preact@10.11.3
-      qr-scanner: 1.4.1
+      qr-scanner: 1.4.2
       qrcode-generator: 1.4.4
       tslib: 2.4.0
     devDependencies:
@@ -10683,6 +10685,10 @@ packages:
       verror: 1.10.0
     dev: true
 
+  /jsqr/1.4.0:
+    resolution: {integrity: 
sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==}
+    dev: false
+
   /jssha/3.3.0:
     resolution: {integrity: 
sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==}
     dev: true
@@ -13210,8 +13216,8 @@ packages:
     engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
     dev: true
 
-  /qr-scanner/1.4.1:
-    resolution: {integrity: 
sha512-xiR90NONHTfTwaFgW/ihlqjGMIZg6ExHDOvGQRba1TvV+WVw7GoDArIOt21e+RO+9WiO4AJJq+mwc5f4BnGH3w==}
+  /qr-scanner/1.4.2:
+    resolution: {integrity: 
sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw==}
     dependencies:
       '@types/offscreencanvas': 2019.7.0
     dev: false

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]