Docs

Quick start

npm i glintcode
import { codeSvg, scanVideo } from "glintcode";

// Generate: an SVG string, on the server or in the browser
const svg = codeSvg("orbit", 123456789);

// Read: from a playing <video>
const scanner = scanVideo(video, { onDetect: (d) => console.log(d.format, d.value) });
scanner.stop();

The package has no dependencies and works in Node, browsers and bundlers. Camera access needs HTTPS or localhost.

Payloads

Every style carries 6 bytes: a number from 0 to 248 − 1, or any 6 bytes as a Uint8Array. Treat it as an ID that your server looks up. That way, what a printed code opens can change after printing.

Pick IDs at random. A code can't prove where it came from, so don't use one as a password or a ticket without a server-side check.

Generate

import { codeSvg } from "glintcode";

codeSvg("capsule", 123456789, { label: { title: "Maison", subtitle: "Table 12" } });
codeSvg("capsule", 123456789, { pill: "#ffffff", marks: "#111111" });
codeSvg("capsule", 123456789, { style: "bare", caption: "Scan with the app" });
codeSvg("orbit", 123456789, { rings: ["#2f37a8", "#3a46cc", "#4451e0", "#4b58e6"] });
codeSvg("halo", 123456789, { ink: "#ffffff", paper: "#111111" });
codeSvg("contour", 123456789, { logo: { href: "/logo.png" }, caption: "Scan me" });

capsuleLayout() and radialModel() return the geometry as data, for native views, canvas or print pipelines.

Style options

OptionStylesMeaning
styleCapsulepill or bare (marks directly on the background)
pill, marks, rows, disc, glyphCapsuleColours; rows takes one colour per row
labelCapsuleText inside the pill: a string or { title, subtitle }
measureTextCapsuleExact text widths; estimated if you leave it out
heightCapsulePill height in px
ink, paper, lens, ringsRoundColours; rings takes one per ring (Halo: per length)
sizeRoundDiameter in px
logoAll"glyph", "none", { href } or { svg }
caption, captionColor, fontFamilyAllText under the code
background, margin, titleAllFill, clear margin in code units, accessible name

Any colours work as long as the marks contrast with what's around them. The generator on the home page checks a colour pair by reading it back.

Read in JavaScript

import { detect, detectImage, scanVideo } from "glintcode";

detect({ data: imageData.data, width, height });     // RGBA or 8-bit luminance
detect(frame, { formats: ["capsule", "orbit"] });    // only some styles
detectImage(imgOrBitmap);                            // a photo: tries several working sizes
scanVideo(video, { onDetect, onFrame, repeatAfter: 3000 });

A detection has format, value, payload, corners, center, size, inverted and corrected (bytes fixed by error correction). detect takes formats, polarity, maxDimension and maxResults. Asking for fewer styles and one polarity is the easiest way to save time per frame.

Swift

Add the package https://github.com/maratmuslimov/glintcode in Xcode or to Package.swift. iOS 15 and macOS 12 or later.

import Glint

let found = Glint.detect(gray: pixels, width: width, height: height)

// From AVCaptureVideoDataOutput (bi-planar YUV or BGRA); call from one queue at a time
let scanner = GlintScanner(maxDimension: 1280)
scanner.onDetect = { detection in print(detection.format, detection.value) }

func captureOutput(_ output: AVCaptureOutput, didOutput buffer: CMSampleBuffer, from connection: AVCaptureConnection) {
  scanner.scan(buffer)
}

The scanner shrinks frames by a whole factor, so use a maxDimension of at least your frame's long side, or a 1280×720 frame is read at 640×360.

Kotlin

The kotlin folder of the repository has two modules: glint (plain JVM, no Android dependency) and glint-android (a CameraX analyzer). They aren't on Maven yet, so add the repository as a submodule and use a Gradle composite build.

// settings.gradle.kts
includeBuild("glintcode/kotlin")    // Android: add glint.android=true to gradle.properties

// build.gradle.kts
implementation("codes.glint:glint-android")
val analysis = ImageAnalysis.Builder().build()
analysis.setAnalyzer(executor, GlintAnalyzer(maxDimension = 1280) { detection ->
  Log.d("glint", "${detection.format} ${detection.value}")
})

Reader parity

The Swift and Kotlin readers are ports of the TypeScript reader, compared with it frame by frame. On the 1,800 frames of the stress run, both returned the same reads as TypeScript, with no differences. Each must also match spec/vectors.json and spec/frames.

ReaderMedian, 960×720Measured on
Swift8.2 msRelease build on an Apple Silicon Mac
Kotlin7.5 msJVM 17 on the same Mac, after warm-up

These timings aren't from phones, and neither port has been measured on a real device yet. The camera helpers were tested with synthetic frames.

Printing

Format

6 data bytes and 6 Reed–Solomon parity bytes over GF(256), XORed with a whitening sequence unique to each style. The reader corrects up to 2 bad bytes, or up to 4 when it knows which ones are unsure, and always keeps one parity byte spare. That makes wrong reads rare, not impossible, so reject IDs your server doesn't know.

The full contract is in spec/FORMAT.md, with conformance data next to it. A new reader passes when it matches spec/vectors.json and spec/frames.