Skip to content

React & Vue

Scanic is framework-agnostic. It works with plain DOM, so it drops into any UI library. Below are idiomatic starting points for React and Vue. The pattern is always the same: turn a file into an HTMLImageElement, pass it to scanDocument, and render result.output.

Upload & scan

jsx
import { useState } from 'react'
import { scanDocument } from 'scanic'

export function DocumentScanner() {
  const [busy, setBusy] = useState(false)
  const [error, setError] = useState(null)
  const [output, setOutput] = useState(null)

  const onFile = (e) => {
    const file = e.target.files[0]
    if (!file) return
    const img = new Image()
    img.onload = async () => {
      setBusy(true)
      setError(null)
      try {
        const result = await scanDocument(img, { mode: 'extract', output: 'dataurl' })
        if (result.success) setOutput(result.output)
        else setError('No document detected')
      } catch (err) {
        setError(err.message)
      } finally {
        setBusy(false)
      }
    }
    img.src = URL.createObjectURL(file)
  }

  return (
    <div>
      <input type="file" accept="image/*" onChange={onFile} disabled={busy} />
      {busy && <p>Scanning…</p>}
      {error && <p>{error}</p>}
      {output && <img src={output} alt="Scanned document" />}
    </div>
  )
}
vue
<script setup>
import { ref } from 'vue'
import { scanDocument } from 'scanic'

const busy = ref(false)
const error = ref(null)
const output = ref(null)

const onFile = (e) => {
  const file = e.target.files[0]
  if (!file) return
  const img = new Image()
  img.onload = async () => {
    busy.value = true
    error.value = null
    try {
      const result = await scanDocument(img, { mode: 'extract', output: 'dataurl' })
      if (result.success) output.value = result.output
      else error.value = 'No document detected'
    } catch (err) {
      error.value = err.message
    } finally {
      busy.value = false
    }
  }
  img.src = URL.createObjectURL(file)
}
</script>

<template>
  <div>
    <input type="file" accept="image/*" @change="onFile" :disabled="busy" />
    <p v-if="busy">Scanning…</p>
    <p v-if="error">{{ error }}</p>
    <img v-if="output" :src="output" alt="Scanned document" />
  </div>
</template>

Live camera scanning

For webcam capture, hold a single Scanner instance across renders so WASM is initialized only once.

jsx
import { useRef, useState, useEffect } from 'react'
import { Scanner } from 'scanic'

export function LiveScanner() {
  const canvasRef = useRef(null)
  const scannerRef = useRef(new Scanner())
  const stateRef = useRef({ stream: null, video: null, raf: 0, running: false })
  const [scanning, setScanning] = useState(false)

  const start = async () => {
    await scannerRef.current.initialize()
    const s = stateRef.current
    s.stream = await navigator.mediaDevices.getUserMedia({ video: true })
    s.video = Object.assign(document.createElement('video'), {
      srcObject: s.stream, autoplay: true, muted: true,
    })
    await new Promise((r) => (s.video.onloadedmetadata = r))
    canvasRef.current.width = s.video.videoWidth
    canvasRef.current.height = s.video.videoHeight
    s.running = true
    setScanning(true)
    loop()
  }

  const loop = async () => {
    const s = stateRef.current
    if (!s.running) return
    const ctx = canvasRef.current.getContext('2d')
    ctx.drawImage(s.video, 0, 0, canvasRef.current.width, canvasRef.current.height)

    if (Math.random() < 0.3) {
      const frame = ctx.getImageData(0, 0, canvasRef.current.width, canvasRef.current.height)
      const result = await scannerRef.current.scan(frame, { mode: 'detect' })
      if (result.success) drawQuad(ctx, result.corners)
    }
    s.raf = requestAnimationFrame(loop)
  }

  const stop = () => {
    const s = stateRef.current
    s.running = false
    cancelAnimationFrame(s.raf)
    s.stream?.getTracks().forEach((t) => t.stop())
    setScanning(false)
  }

  useEffect(() => stop, [])

  return (
    <div>
      <button onClick={start} disabled={scanning}>Start</button>
      <button onClick={stop} disabled={!scanning}>Stop</button>
      <canvas ref={canvasRef} />
    </div>
  )
}

function drawQuad(ctx, c) {
  ctx.strokeStyle = '#22c55e'
  ctx.lineWidth = 3
  ctx.beginPath()
  ctx.moveTo(c.topLeft.x, c.topLeft.y)
  ctx.lineTo(c.topRight.x, c.topRight.y)
  ctx.lineTo(c.bottomRight.x, c.bottomRight.y)
  ctx.lineTo(c.bottomLeft.x, c.bottomLeft.y)
  ctx.closePath()
  ctx.stroke()
}
vue
<script setup>
import { ref, onUnmounted } from 'vue'
import { Scanner } from 'scanic'

const scanner = new Scanner()
const canvas = ref(null)
const scanning = ref(false)
let stream = null, video = null, raf = 0, running = false

const start = async () => {
  await scanner.initialize()
  stream = await navigator.mediaDevices.getUserMedia({ video: true })
  video = Object.assign(document.createElement('video'), {
    srcObject: stream, autoplay: true, muted: true,
  })
  await new Promise((r) => (video.onloadedmetadata = r))
  canvas.value.width = video.videoWidth
  canvas.value.height = video.videoHeight
  running = true
  scanning.value = true
  loop()
}

const loop = async () => {
  if (!running) return
  const ctx = canvas.value.getContext('2d')
  ctx.drawImage(video, 0, 0, canvas.value.width, canvas.value.height)
  if (Math.random() < 0.3) {
    const frame = ctx.getImageData(0, 0, canvas.value.width, canvas.value.height)
    const result = await scanner.scan(frame, { mode: 'detect' })
    if (result.success) {
      const c = result.corners
      ctx.strokeStyle = '#22c55e'
      ctx.lineWidth = 3
      ctx.beginPath()
      ctx.moveTo(c.topLeft.x, c.topLeft.y)
      ctx.lineTo(c.topRight.x, c.topRight.y)
      ctx.lineTo(c.bottomRight.x, c.bottomRight.y)
      ctx.lineTo(c.bottomLeft.x, c.bottomLeft.y)
      ctx.closePath()
      ctx.stroke()
    }
  }
  raf = requestAnimationFrame(loop)
}

const stop = () => {
  running = false
  cancelAnimationFrame(raf)
  stream?.getTracks().forEach((t) => t.stop())
  scanning.value = false
}

onUnmounted(stop)
</script>

<template>
  <div>
    <button @click="start" :disabled="scanning">Start</button>
    <button @click="stop" :disabled="!scanning">Stop</button>
    <canvas ref="canvas" />
  </div>
</template>

TIP

Want users to fine-tune detected corners before capture? Drop in the Corner Editor, it works in any framework.

Released under the MIT License.