Osy#betaRun it on Osyrin: osyrin.com

Kits · UI

Barcode scanner

A live barcode + QR scanner: it owns the camera, decoding with ZXing.

Package
Osysharp.Barcode
Install
use Osysharp.Barcode@1;
Group
UI
Tests
—
Builds on
—
Source
github.com/osysharp/barcode

A control that opens the camera, reads QR codes and barcodes as they come into view, and tells your app what it read. Decoding runs in the browser with ZXing compiled to WebAssembly — no scanning service, and nothing leaves the device.

Use case

Any app where someone points a phone at a code instead of typing it: checking tickets in at an event door, receiving stock in a warehouse by scanning the carton's barcode, looking up an asset by the QR label on it, pairing a device from the code on its box. The control owns the camera and the decode loop; your app supplies a box to draw it in and an action to run with what it read.

There is no screenshot here: what the scanner shows is your camera's live picture, and a picture of a test pattern would say nothing true about it.

Install

osy

// app.osy
app Warehouse {
  use Osysharp.Barcode@1;
  model "model/**/*.osy";
}

console

osy lock      # resolves the package and pins its content address in osyrin.lock

Using it

osy

using Osysharp.Barcode;

action Found(string text, string format) { code = text; }

render {
  Stack(h: "16rem") {
    BarcodeScanner(scanned: Found, formats: qr);
  }
}

That is the whole surface. The control opens the camera, watches the picture, and raises scanned(text, format) when it reads something. It owns the camera and the decode loop; the app supplies a box and an action.

That is the whole surface. scanned(text, format) fires once per code — holding a label in view does not repeat it — and failed(reason) fires once per cause, such as a camera the person did not allow.

What it ships

filewhat it is
scan.osythe control BarcodeScanner declaration — props, events, chunks, probe
scan.ts → scan.jsthe shim (3 KB): the camera, the frame loop, the decoder handshake
zxing-core.jschunk Core — zxing-wasm's Emscripten glue, 37 KB
zxing-reader.wasmchunk Wasm — ZXing's C++ decoder compiled to WebAssembly, 1.07 MB

A page that never activates a scanner downloads neither chunk.

A page that never activates a scanner downloads neither chunk.

Props worth knowing about

propdefaultwhy it exists
activetruefalse releases the camera and stops the loop — the browser's recording indicator goes out, which is the only signal a person has
formatsanyqr · linear · any. ZXing spends time proportional to the families it must try, so narrowing is materially faster on a phone
facingbacka scanner is pointed at something. A preference, not a demand — a laptop with one camera ignores it
intervalMs250four looks a second. Not a frame rate: decoding is synchronous work on the main thread
repeatAfterMs2000the prop that makes it usable. A label held in shot is read on every pass; without suppression one label produces forty events. 0 opts out

Build your own on top

Wrap it. The scanner is one call, so a component of yours can own the rest of the flow — a TicketCheckIn that scans, looks the code up, and shows a green or red result — and your pages use that.

Fork it. Fork this repository into your own GitHub account or organisation and rename the package to match — package Acme.Barcode lives at github.com/acme/barcode, because a package's owner is its GitHub owner. The control is a small TypeScript shim over zxing-wasm, built with the ordinary CLI:

console

npm install
npx playwright install chromium   # once — the browser `npm run verify` drives
npm run chunks      # rebuild the Core and Wasm chunks from node_modules
osy control build   # regenerate the typed ABI, type-check the shim against it, bundle it, re-pin osyrin.lock
npm run verify      # a real Chromium, a real camera stream carrying a real QR code
osy publish         # check the package and create the release

npm run verify is the measurement this kit rests on. It serves the kit's own chunks with the platform's exact page policy, encodes a QR with ZXing's writer, paints it onto a canvas, hands the control that canvas's captureStream() as its camera, and requires:

  • exactly one scanned event carrying the payload and QRCode — one, not one per frame, which is the repeat suppression doing its job;
  • destroy() leaving no live camera track;
  • and then it runs the whole thing again with 'wasm-unsafe-eval' removed and requires the scanner to fail, so a

fork that breaks how the decoder loads fails here rather than on someone's phone.

How the decoder is loaded

Everything hard is in one function, loadDecoder in scan.ts:

ts

const mod = await import(host.chunkUrl('Core'));
await mod.prepareZXingModule({
  overrides: { locateFile: () => host.chunkUrl('Wasm') },   // ← the whole kit turns on this line
  fireImmediately: true,
});

Emscripten glue does not embed its module; it asks, at run time, through locateFile(). zxing-wasm's default answer is a jsDelivr URL — which an app page may not fetch (default-src 'self', correctly). Handing it a same-origin content address is what makes a native library a first-class control asset instead of a CDN dependency.

A page that runs it needs a content-security policy that admits WebAssembly ('wasm-unsafe-eval'); an Osy# app's pages already carry it.

Two shapes in the shim worth not undoing

The loop is chained, not an interval. The next look is scheduled when the previous one finishes, so there is never more than one decode in flight — by construction, with no flag to get wrong. setInterval would queue a second decode on a device that fell behind, and then a third.

failed is raised once per cause. The loop runs four times a second and a dead camera fails on every pass; an unguarded emit is four toasts a second for as long as the page is open.

Licence

MIT — see LICENSE. The .osy source is yours to read, copy, fork and ship.

ZXing (zxing-cpp) is Apache-2.0; zxing-wasm is MIT. Both are vendored as built artifacts, pinned by content address in osyrin.lock, and keep their own terms.

Get it

Add the kit to your app’s manifest:

osy

app MyApp {
  use Osysharp.Barcode@1;
}

Then run osy lock: it downloads the release, verifies it by its content address and pins that version in osyrin.lock. osy kits Osysharp.Barcode shows what the kit is for, from inside your app.

Source: github.com/osysharp/barcode.

Reference: osy docs ui-barcode-kit.