GrCodeScanner

Package: @feugene/granularity-mediacompanionGroup: misc

Machine-translated from the Russian original, not yet reviewed. Read the original

When to take it

  • signing in with a QR — on the login screen of another device, instead of typing a code by hand;
  • a product in a warehouse — the receiving desk scans barcodes one after another, and continuous is exactly for that;
  • a payment or a link from a poster — the code leads to a page, and the application opens it;
  • pairing a device — the code is shown by a second screen, and the phone confirms it.

When to take something else

NeedTake
Take a photo rather than read a codeGrCameraCapture
Choose a crop out of a ready pictureGrImageCrop
Accept a file with a shot of a codeGrFileUpload

The package does not carry a decoder

The native BarcodeDetector exists in Chrome and Edge, but it is in neither Safari nor Firefox — that is, on an iPhone it does not exist at all, while scanning is most often done precisely with a phone. Embedding a decoder into the package is not possible: it has not a single dependency, and for the sake of one component the heaviest one would appear — and for everyone, including those who took only the cropper.

The component therefore handles the native path itself, and the other browsers are covered by a detector the application passes in:

import { BrowserMultiFormatReader } from '@zxing/browser'

const reader = new BrowserMultiFormatReader()

async function detector(source) {
  const canvas = Object.assign(document.createElement('canvas'), {
    width: source.videoWidth,
    height: source.videoHeight,
  })
  canvas.getContext('2d')?.drawImage(source, 0, 0)

  const result = reader.decodeFromCanvas(canvas)

  return result ? [{ value: result.getText(), format: String(result.getBarcodeFormat()) }] : []
}
<GrCodeScanner :detector="detector" />

The prop is deliberately stronger than the native path: an application that has connected a library of its own usually does so for the sake of a format the native detector does not know.

There being nothing to read with is a separate state: the component says straight out that the browser cannot do it and that a detector is needed. Offering “switch the camera on” in that case would mean sending the user to solve the wrong task.

One code in the frame is one event

The camera gives away dozens of frames per second, and the code is recognised in every one of them. Without a filter the application would receive a stream of identical events and would place twenty orders instead of one, so only what was not in the previous frame is reported.

The symbology is part of the identity: one and the same value in qr_code and in ean_13 counts as different codes, because it means different things too.

continuous removes the filter: at a receiving desk identical packages are scanned one after another, and there a repeat is a legitimate second event.

The parsing goes by a timer rather than on every frame

interval (250 ms by default) is a trade between responsiveness and heating: parsing every frame means keeping the processor busy continuously, and a phone in the hand heats up noticeably.

A frame that did not parse is the norm rather than a failure: a hand, a reflection, a blur got into the lens. Such a pass is skipped silently, and the next one will come after interval.

Limits

  • there is no parsing of a picture from a file. The component works with a live stream; for a shot of a code the application calls its detector directly;
  • there is no torch and no zoom. torch and zoom are extensions of the track, and their support differs even within Chrome;
  • there is no marking of the found code on the preview. The native API gives away the coordinates, but someone else’s detector does not necessarily, and drawing a frame for half of the sources would mean promising what is not there.

Install

npm i @feugene/granularity-media

Import

import { GrCodeScanner } from '@feugene/granularity-media/components/GrCodeScanner'

API

The API for this component has not been generated yet: the showcase generator only covers the core so far. Until it does, the reference lives in the package documentation.

Examples 2

Basic

Basic
<script setup lang="ts">
import { ref } from 'vue'

import { GrBadge, GrSwitch } from '@feugene/granularity'
import type { GrCodeResult } from '@feugene/granularity-media'

const found = ref<GrCodeResult[]>([])
const continuous = ref(false)

function onDetect(codes: GrCodeResult[]) {
  // Свежие коды приходят пачкой: в кадр попадает и наклейка, и ценник рядом.
  found.value = [...codes, ...found.value].slice(0, 8)
}
</script>

<template>
  <div class="grid gap-4 lg:grid-cols-2">
    <GrCodeScanner :continuous="continuous" @detect="onDetect" />

    <div class="showcase-demo-panel grid content-start gap-3 rounded-[var(--gr-radius-lg)] border p-4">
      <GrSwitch v-model="continuous" size="sm">
        Сообщать повторы
      </GrSwitch>

      <p class="showcase-demo-text text-sm">
        Без этого один код в кадре даёт одно событие: камера отдаёт десятки кадров в секунду,
        и приложение оформило бы двадцать заказов вместо одного. На приёмке, где сканируют
        одинаковые упаковки подряд, повтор — законное второе событие.
      </p>

      <template v-if="found.length > 0">
        <p class="showcase-demo-text text-sm">
          Найдено:
        </p>
        <ul class="grid gap-2">
          <li v-for="code in found" :key="`${code.format}:${code.value}`" class="flex items-center gap-2">
            <GrBadge size="sm" tone="neutral">{{ code.format }}</GrBadge>
            <code class="showcase-demo-text text-sm">{{ code.value }}</code>
          </li>
        </ul>
      </template>
      <p v-else class="showcase-demo-text text-sm">
        Наведите камеру на QR или штрихкод — содержимое появится здесь.
      </p>
    </div>
  </div>
</template>

Receiving

Receiving
<script setup lang="ts">
import { computed, ref } from 'vue'

import { GrBadge, GrButton, GrEmptyState, GrTable } from '@feugene/granularity'
import type { GrCodeResult } from '@feugene/granularity-media'

/**
 * Приёмка: коробки сканируют подряд, и одинаковых среди них большинство.
 *
 * Здесь видно, зачем `continuous`: без него второй такой же штрихкод не дал бы
 * события вовсе, и кладовщик решил бы, что сканер не сработал. С ним повтор —
 * законная вторая единица товара.
 */
/**
 * Справочник — `Map`, а не объект: ключ штрихкода это строка, и у объекта она
 * превратилась бы в число. Код с ведущим нулём (UPC-A, записанный как EAN-13)
 * потерял бы его молча — и позиция перестала бы находиться.
 */
const CATALOG = new Map([
  ['4600051000057', 'Кофе зерновой, 1 кг'],
  ['5901234123457', 'Бумага А4, 500 л'],
  ['4008400402222', 'Батарейки AA, 4 шт'],
])

interface Position {
  code: string
  title: string
  count: number
}

const positions = ref<Position[]>([])

const total = computed(() => positions.value.reduce((sum, item) => sum + item.count, 0))

function onDetect(codes: GrCodeResult[]) {
  for (const code of codes) {
    const found = positions.value.find(item => item.code === code.value)

    if (found) {
      found.count += 1
      continue
    }

    positions.value = [
      { code: code.value, title: CATALOG.get(code.value) ?? 'Неизвестная позиция', count: 1 },
      ...positions.value,
    ]
  }
}
</script>

<template>
  <div class="grid gap-4 lg:grid-cols-2">
    <div class="grid gap-3">
      <GrCodeScanner continuous :formats="['ean_13', 'code_128', 'qr_code']" @detect="onDetect" />
      <p class="showcase-demo-text text-sm">
        Наводите на штрихкоды подряд — повтор увеличивает количество, новый код добавляет строку.
      </p>
    </div>

    <div class="grid content-start gap-3">
      <div class="flex items-center justify-between gap-2">
        <span class="text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600">
          Принято: {{ total }} шт
        </span>
        <GrButton
          size="xs"
          variant="outline"
          :disabled="positions.length === 0"
          @click="positions = []"
        >
          Очистить
        </GrButton>
      </div>

      <GrTable v-if="positions.length > 0" :column-count="3">
        <template #header>
          <tr>
            <th class="text-left">
              Позиция
            </th>
            <th class="text-left">
              Код
            </th>
            <th class="text-right">
              Кол-во
            </th>
          </tr>
        </template>

        <tr v-for="item in positions" :key="item.code">
          <td>{{ item.title }}</td>
          <td><code class="showcase-demo-text text-xs">{{ item.code }}</code></td>
          <td class="text-right">
            <GrBadge size="sm" :tone="item.count > 1 ? 'success' : 'neutral'">
              {{ item.count }}
            </GrBadge>
          </td>
        </tr>
      </GrTable>

      <GrEmptyState
        v-else
        title="Пока ничего не отсканировано"
        description="Три кода из справочника опознаются по названию, остальные попадут как «Неизвестная позиция» — на складе это отдельная задача приёмщика."
      />
    </div>
  </div>
</template>

Component documentationAll components