paulmillr / qr

Minimal browser & node.js QR Code Pattern reader and generator
https://paulmillr.com/apps/qr/
Apache License 2.0
147 stars 5 forks source link
qr qr-code qrcode qrcode-generator qrcode-reader qrcode-scanner

paulmillr-qr

Minimal browser and node.js QR Code Pattern encoder & decoder.

Check out interactive demo paulmillr.com/apps/qr/ and qrBTF.com, which uses the library to generate custom, styled codes.

Other JS libraries are bad:

Usage

A standalone file paulmillr-qr.js is also available.

npm install @paulmillr/qr

Encoding

import encodeQR from '@paulmillr/qr';

// import decodeQR from '@paulmillr/qr/decode';
// See separate README section for decoding.

const txt = 'Hello world';
const ascii = encodeQR(txt, 'ascii'); // Not all fonts are supported
const terminalFriendly = encodeQR(txt, 'term'); // 2x larger, all fonts are OK
const gifBytes = encodeQR(txt, 'gif'); // Uncompressed GIF
const svgElement = encodeQR(txt, 'svg'); // SVG vector image element
const array = encodeQR(txt, 'raw'); // 2d array for canvas or other libs

// Options
// Custom error correction level
// low: 7%, medium: 15% (default), quartile: 25%, high: 30%
const highErrorCorrection = encodeQR(txt, 'gif', { ecc: 'high' });
// Custom encoding: 'numeric', 'alphanumeric' or 'byte'
const customEncoding = encodeQR(txt, 'gif', { encoding: 'byte' });
// Default scale is 2: each block is 2x2 pixels.
const larger = encodeQR(txt, 'gif', { scale: 4 });
// All options
// type QrOpts = {
//   ecc?: 'low' | 'medium' | 'quartile' | 'high';
//   encoding?: 'numeric' | 'alphanumeric' | 'byte' | 'kanji' | 'eci';
//   version?: number; // 1..40, QR code version
//   mask?: number; // 0..7, mask number
//   border?: number; // Border size, default 2.
//   scale?: number; // Scale to this number. Scale=2 -> each block will be 2x2 pixels
// };

console.log(ascii);
> β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
> β–ˆβ–ˆ β–„β–„β–„β–„β–„ β–ˆ  β–€β–„β–„β–ˆ β–ˆβ–ˆβ–€β–„β–„β–„β–„β–ˆ β–€β–ˆ β–„β–„β–„β–„β–„ β–ˆβ–ˆ
> β–ˆβ–ˆ β–ˆ   β–ˆ β–ˆβ–€β–„β–€β–„ β–„β–„β–ˆβ–„β–ˆ β–ˆβ–ˆβ–€β–ˆβ–€β–€β–ˆ β–ˆ   β–ˆ β–ˆβ–ˆ
> β–ˆβ–ˆ β–ˆβ–„β–„β–„β–ˆ β–ˆβ–ˆ β–„β–„β–ˆβ–„β–€β–€ β–€ β–ˆβ–ˆ β–„ β–„β–ˆ β–ˆβ–„β–„β–„β–ˆ β–ˆβ–ˆ
> β–ˆβ–ˆβ–„β–„β–„β–„β–„β–„β–„β–ˆ β–€ β–€ β–ˆβ–„β–€ β–€ β–€β–„β–ˆ β–ˆ β–ˆβ–„β–„β–„β–„β–„β–„β–„β–ˆβ–ˆ
> β–ˆβ–ˆ β–ˆ  β–€ β–„β–„β–€β–€β–€ β–ˆβ–€ β–„   β–€β–€β–„β–€ β–„β–ˆ β–€β–ˆ β–€β–„β–„β–ˆβ–ˆ
> β–ˆβ–ˆβ–€β–€β–€  β–€β–„β–„β–ˆβ–ˆβ–„β–€β–€β–„β–ˆβ–€ β–€β–„β–ˆ    β–€β–€β–€ β–„ β–ˆβ–„β–„β–ˆβ–ˆ
> β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–„β–€β–€β–„β–„β–ˆβ–ˆ β–€ β–€ β–„β–„β–ˆβ–ˆβ–„ β–„β–„ β–„ β–ˆβ–€β–ˆ β–ˆ β–ˆβ–ˆβ–ˆ
> β–ˆβ–ˆβ–ˆ   β–„β–€β–„β–ˆβ–„β–„β–„β–ˆ   β–€β–ˆβ–ˆβ–„β–„β–„β–€β–€β–ˆβ–„β–€ β–„β–ˆβ–€ β–ˆβ–ˆβ–ˆβ–ˆ
> β–ˆβ–ˆβ–€β–€ β–„ β–€β–„ β–„β–„β–ˆβ–ˆβ–€β–„β–€β–€β–ˆβ–ˆβ–ˆβ–ˆβ–„β–„β–„ β–ˆβ–„ β–ˆ  β–ˆβ–€β–€β–ˆβ–ˆ
> β–ˆβ–ˆβ–€β–€β–„ β–„β–€β–„ β–€β–€β–ˆβ–„β–€β–€β–„β–„β–€β–€ β–ˆβ–„β–„β–€β–ˆβ–€ β–€β–„ β–ˆβ–„ β–€β–ˆβ–ˆ
> β–ˆβ–ˆβ–€β–„β–€β–ˆβ–ˆ β–„β–„ β–€β–ˆβ–„β–ˆβ–€ β–€ β–€β–ˆβ–„β–€β–€ β–ˆβ–„β–€β–€ β–ˆ  β–ˆ β–ˆβ–ˆ
> β–ˆβ–ˆβ–ˆβ–€β–ˆβ–„β–€β–„β–„ β–ˆ  β–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–„ β–ˆ β–„β–„β–„ β–„β–€β–€β–„β–„ β–ˆβ–ˆ
> β–ˆβ–ˆβ–„β–ˆβ–„β–„β–„β–ˆβ–„β–ˆ β–„ β–„β–€β–ˆβ–€β–€ β–„β–€ β–ˆβ–€ β–„ β–„β–„β–„ β–€β–„β–€β–„β–ˆβ–ˆ
> β–ˆβ–ˆ β–„β–„β–„β–„β–„ β–ˆ β–„β–ˆβ–„β–€β–€ β–€β–ˆ   β–ˆβ–„β–ˆ  β–ˆβ–„β–ˆ β–€β–€β–„β–€β–ˆβ–ˆ
> β–ˆβ–ˆ β–ˆ   β–ˆ β–ˆβ–€ β–„β–€β–ˆ β–ˆβ–ˆ β–„β–„β–€β–ˆβ–ˆ   β–„β–„ β–„β–ˆ   β–ˆβ–ˆ
> β–ˆβ–ˆ β–ˆβ–„β–„β–„β–ˆ β–ˆβ–„  β–ˆβ–ˆβ–€ β–„β–„ β–€β–ˆ β–„      β–€β–„β–„β–ˆβ–€β–ˆβ–ˆ
> β–ˆβ–ˆβ–„β–„β–„β–„β–„β–„β–„β–ˆβ–„β–ˆβ–ˆβ–ˆβ–„β–ˆβ–„β–ˆβ–„β–„β–„β–„β–ˆβ–„β–ˆβ–„β–ˆβ–ˆβ–ˆβ–ˆβ–„β–„β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
> β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€

Decoding

GIF reader is not included in the package (it would take a lot of space). Decoding raw bitmap is still possible.

import encodeQR from '@paulmillr/qr';
import decodeQR from '@paulmillr/qr/decode.js';
import { Bitmap } from '@paulmillr/qr';

// Scale so it would be 100x100 instead of 25x25
const opts = { scale: 4 };

// a) Decode using raw bitmap, dependency-free
function decodeRawBitmap() {
  const bmBits = encodeQR('Hello world', 'raw', opts);
  const bm = new Bitmap({ width: bmBits[0].length, height: bmBits.length });
  bm.data = bmBits;
  const decoded = decodeQR(bm.toImage());
  console.log('decoded(pixels)', decoded);
}
/*
Output:
decoded(pixels) Hello world
decoded(gif) Hello world
*/

// b) Decode using external GIF decoder
import gif from 'omggif'; // npm install omggif@1.0.10
function parseGIF(image) {
  const r = new gif.GifReader(image);
  const data = [];
  r.decodeAndBlitFrameRGBA(0, data);
  const { width, height } = r.frameInfo(0);
  return { width, height, data };
}
function decodeWithExternal() {
  const gifBytes = encodeQR('Hello world', 'gif', opts);
  const decoded = decodeQR(parseGIF(gifBytes));
  console.log('decoded(gif)', decoded);
}

// c) draw gif/svg to browser DOM canvas
import { svgToPng } from '@paulmillr/qr/dom.js';
const png = svgToPng(encodeQR('Hello world', 'svg'), 512, 512);

Decoding options

export type Point4 = { x: number; y: number }[];
export type Image = {
  height: number;
  width: number;
  data: Uint8Array | Uint8ClampedArray | number[];
};
export type DecodeOpts = {
  // By default we assume that image has 4 channel per pixel (RGBA). isRGB: true will force to use only one
  isRGB?: boolean;
  // Returns 4 center (3 finder pattern + 1 alignment pattern) points if detected
  detectFn?: (points: Point4) => void;
  // Returns RGBA image of detected QR code
  qrFn?: (img: Image) => void;
};
export default function decodeQR(img: Image, opts: DecodeOpts = {});

Decoding algorithm

QR code decoding is challenging; it is essentially a computer vision problem. There are two main scenarios:

The state-of-the-art approach for this, as with other computer vision problems, is using neural networks. However, using them would make the library hard to audit. Additionally, since JavaScript can't access hardware accelerators, it would likely be very slow. We also avoid using WebGL because it is complex and exposes users to fingerprinting.

The implemented reader algorithm is inspired by ZXing:

  1. toBitmap: Convert the image to a bitmap of black and white segments. This is the slowest part and the most important.
  2. detect: Find three finder patterns and one alignment pattern (for versions > 1). This is trickyβ€”they can be rotated and distorted by perspective. A square might appear as a quadrilateral with unknown size. The best we can do is count runs of the same color and select patterns with almost the same ratio of runs.
  3. transform: Once patterns have been found, correct the perspective and transform the quadrilateral into a square.
  4. decodeBitmap: Execute the encoding in reverse: read information via a zig-zag pattern, de-interleave bytes, correct errors, convert to bits, and finally, read segments from bits to create the string.
  5. Finished!

Decoding test vectors

To test our QR code decoding, we use an excellent dataset from BoofCV. BoofCV decodes 73% of the test cases, while ZXing decodes 49%. Our implementation is nearly at parity with ZXing, primarily because ECI (Extended Channel Interpretation) support is not yet included. The test vectors are preserved in a Git repository at github.com/paulmillr/qr-code-vectors.

Note for Testing on iOS Safari: Accessing the camera on iOS Safari requires HTTPS. This means that the file: protocol or non-encrypted http cannot be used. Ensure your testing environment uses https:.

The QR code specification is available for purchase at iso.org for 200 CHF.

DOM helpers for web apps

Check out dom.ts for browser-related camera code that would make your apps simpler.

Using with Kotlin

@JsModule("@paulmillr/qr")
@JsNonModule
external object Qr {
    @JsName("default")
    fun encodeQR(text: String, output: String = definedExternally, opts: dynamic = definedExternally): Uint8Array
}

// then
val bytes = Qr.encodeQR("text", "gif", js("{ scale: 10 }"))
val blob = Blob(arrayOf(bytes), BlobPropertyBag("image/gif"))
val imgSrc = URL.createObjectURL(blob)

Security

There are multiple ways a single text can be encoded in a QR code, which can lead to potential security implications:

If an adversary can access multiple QR codes generated from a specific library, they may be able to fingerprint the user. This fingerprinting could be used to exfiltrate data from air-gapped systems. In such cases, the adversary would need to create a library-specific exploit.

We mitigate these risks by:

Future plans:

Speed

Benchmarks measured with Apple M2 on MacOS 13 with node.js 19.

======== encode/ascii ========
encode/paulmillr-qr x 1,794 ops/sec @ 557ΞΌs/op
encode/qrcode-generator x 3,128 ops/sec @ 319ΞΌs/op Β± 1.12% (min: 293ΞΌs, max: 3ms)
encode/nuintun x 1,872 ops/sec @ 533ΞΌs/op
======== encode/gif ========
encode/paulmillr-qr x 1,771 ops/sec @ 564ΞΌs/op
encode/qrcode-generator x 1,773 ops/sec @ 563ΞΌs/op
encode/nuintun x 1,883 ops/sec @ 530ΞΌs/op
======== encode: big ========
encode/paulmillr-qr x 87 ops/sec @ 11ms/op
encode/qrcode-generator x 124 ops/sec @ 8ms/op
encode/nuintun x 143 ops/sec @ 6ms/op
======== decode ========
decode/paulmillr-qr x 96 ops/sec @ 10ms/op Β± 1.39% (min: 9ms, max: 32ms)
decode/jsqr x 34 ops/sec @ 28ms/op
decode/nuintun x 35 ops/sec @ 28ms/op
decode/instascan x 79 ops/sec @ 12ms/op Β± 6.73% (min: 9ms, max: 223ms)
======== Decoding quality ========
blurred(45):  paulmillr-qr=12 (26.66%) jsqr=13 (28.88%) nuintun=13 (28.88%) instascan=11 (24.44%)

License

Copyright (c) 2023 Paul Miller (paulmillr.com)

Copyright (c) 2019 ZXing authors

The library @paulmillr/qr is dual-licensed under the Apache 2.0 OR MIT license. You can select a license of your choice.

The library contains code inspired by ZXing, which is licensed under Apache 2.0.

The license to the use of the QR Code stipulated by JIS (Japanese Industrial Standards) and the ISO are not necessary. The specification for QR Code has been made available for use by any person or organization. (Obtaining QR Code Specification) The word β€œQR Code” is registered trademark of DENSO WAVE INCORPORATED in Japan and other countries. To use the word β€œQR Code” in your publications or web site, etc, please indicate a sentence QR Code is registered trademark of DENSO WAVE INCORPORATED. This registered trademark applies only for the word β€œQR Code”, and not for the QR Code pattern (image). (https://www.qrcode.com/en/faq.html)