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
| Option | Styles | Meaning |
|---|---|---|
style | Capsule | pill or bare (marks directly on the background) |
pill, marks, rows, disc, glyph | Capsule | Colours; rows takes one colour per row |
label | Capsule | Text inside the pill: a string or { title, subtitle } |
measureText | Capsule | Exact text widths; estimated if you leave it out |
height | Capsule | Pill height in px |
ink, paper, lens, rings | Round | Colours; rings takes one per ring (Halo: per length) |
size | Round | Diameter in px |
logo | All | "glyph", "none", { href } or { svg } |
caption, captionColor, fontFamily | All | Text under the code |
background, margin, title | All | Fill, 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.
| Reader | Median, 960×720 | Measured on |
|---|---|---|
| Swift | 8.2 ms | Release build on an Apple Silicon Mac |
| Kotlin | 7.5 ms | JVM 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
- Keep a clear margin around the code. The default margin is the minimum.
- In the frame the reader sees, a Capsule should be at least about 32 px tall and a round code at least 60 px wide. Contour needs about 150 px.
- Test on the real material, under the real light, with the phones your users have. The numbers on the home page don't cover print.
- Avoid glossy laminate where you can. Glare hides marks in a way that error correction may not recover.
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.