Giter VIP home page Giter VIP logo

qr's Introduction

paulmillr-qr

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

  • πŸ”’ Auditable, 0-dependency
  • 🏞️ Encoding supports generating ASCII, term, gif and svg codes
  • πŸ“· Decoding supports reading from camera feed input, files and non-browser environments
  • πŸ” Extensive tests ensure correctness: 100MB+ of vectors
  • πŸͺΆ Just 1000 lines for encoding and 800 lines for reading

Interactive demo is available at paulmillr.com/demos/qr/.

Other JS libraries are bad, they:

  • Don't work: jsQR is dead, zxing-js is dead, qr-scanner uses jsQR, doesn't work outside of browser, qcode-decoder broken version of jsQR, doesn't work outside of browser, qrcode modern refactor of jsQR (138 stars)
  • Too big: instascan is 1MB+ (zxing compiled to js via emscripten)

Usage

npm install @paulmillr/qr

import encodeQR from '@paulmillr/qr';
const gifBytes = encodeQR('Hello world', 'gif');

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

console.log(encodeQR('Hello world', 'ascii'));
> β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
> β–ˆβ–ˆ β–„β–„β–„β–„β–„ β–ˆ  β–€β–„β–„β–ˆ β–ˆβ–ˆβ–€β–„β–„β–„β–„β–ˆ β–€β–ˆ β–„β–„β–„β–„β–„ β–ˆβ–ˆ
> β–ˆβ–ˆ β–ˆ   β–ˆ β–ˆβ–€β–„β–€β–„ β–„β–„β–ˆβ–„β–ˆ β–ˆβ–ˆβ–€β–ˆβ–€β–€β–ˆ β–ˆ   β–ˆ β–ˆβ–ˆ
> β–ˆβ–ˆ β–ˆβ–„β–„β–„β–ˆ β–ˆβ–ˆ β–„β–„β–ˆβ–„β–€β–€ β–€ β–ˆβ–ˆ β–„ β–„β–ˆ β–ˆβ–„β–„β–„β–ˆ β–ˆβ–ˆ
> β–ˆβ–ˆβ–„β–„β–„β–„β–„β–„β–„β–ˆ β–€ β–€ β–ˆβ–„β–€ β–€ β–€β–„β–ˆ β–ˆ β–ˆβ–„β–„β–„β–„β–„β–„β–„β–ˆβ–ˆ
> β–ˆβ–ˆ β–ˆ  β–€ β–„β–„β–€β–€β–€ β–ˆβ–€ β–„   β–€β–€β–„β–€ β–„β–ˆ β–€β–ˆ β–€β–„β–„β–ˆβ–ˆ
> β–ˆβ–ˆβ–€β–€β–€  β–€β–„β–„β–ˆβ–ˆβ–„β–€β–€β–„β–ˆβ–€ β–€β–„β–ˆ    β–€β–€β–€ β–„ β–ˆβ–„β–„β–ˆβ–ˆ
> β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–„β–€β–€β–„β–„β–ˆβ–ˆ β–€ β–€ β–„β–„β–ˆβ–ˆβ–„ β–„β–„ β–„ β–ˆβ–€β–ˆ β–ˆ β–ˆβ–ˆβ–ˆ
> β–ˆβ–ˆβ–ˆ   β–„β–€β–„β–ˆβ–„β–„β–„β–ˆ   β–€β–ˆβ–ˆβ–„β–„β–„β–€β–€β–ˆβ–„β–€ β–„β–ˆβ–€ β–ˆβ–ˆβ–ˆβ–ˆ
> β–ˆβ–ˆβ–€β–€ β–„ β–€β–„ β–„β–„β–ˆβ–ˆβ–€β–„β–€β–€β–ˆβ–ˆβ–ˆβ–ˆβ–„β–„β–„ β–ˆβ–„ β–ˆ  β–ˆβ–€β–€β–ˆβ–ˆ
> β–ˆβ–ˆβ–€β–€β–„ β–„β–€β–„ β–€β–€β–ˆβ–„β–€β–€β–„β–„β–€β–€ β–ˆβ–„β–„β–€β–ˆβ–€ β–€β–„ β–ˆβ–„ β–€β–ˆβ–ˆ
> β–ˆβ–ˆβ–€β–„β–€β–ˆβ–ˆ β–„β–„ β–€β–ˆβ–„β–ˆβ–€ β–€ β–€β–ˆβ–„β–€β–€ β–ˆβ–„β–€β–€ β–ˆ  β–ˆ β–ˆβ–ˆ
> β–ˆβ–ˆβ–ˆβ–€β–ˆβ–„β–€β–„β–„ β–ˆ  β–ˆ β–ˆβ–ˆ β–ˆβ–ˆ β–„ β–ˆ β–„β–„β–„ β–„β–€β–€β–„β–„ β–ˆβ–ˆ
> β–ˆβ–ˆβ–„β–ˆβ–„β–„β–„β–ˆβ–„β–ˆ β–„ β–„β–€β–ˆβ–€β–€ β–„β–€ β–ˆβ–€ β–„ β–„β–„β–„ β–€β–„β–€β–„β–ˆβ–ˆ
> β–ˆβ–ˆ β–„β–„β–„β–„β–„ β–ˆ β–„β–ˆβ–„β–€β–€ β–€β–ˆ   β–ˆβ–„β–ˆ  β–ˆβ–„β–ˆ β–€β–€β–„β–€β–ˆβ–ˆ
> β–ˆβ–ˆ β–ˆ   β–ˆ β–ˆβ–€ β–„β–€β–ˆ β–ˆβ–ˆ β–„β–„β–€β–ˆβ–ˆ   β–„β–„ β–„β–ˆ   β–ˆβ–ˆ
> β–ˆβ–ˆ β–ˆβ–„β–„β–„β–ˆ β–ˆβ–„  β–ˆβ–ˆβ–€ β–„β–„ β–€β–ˆ β–„      β–€β–„β–„β–ˆβ–€β–ˆβ–ˆ
> β–ˆβ–ˆβ–„β–„β–„β–„β–„β–„β–„β–ˆβ–„β–ˆβ–ˆβ–ˆβ–„β–ˆβ–„β–ˆβ–„β–„β–„β–„β–ˆβ–„β–ˆβ–„β–ˆβ–ˆβ–ˆβ–ˆβ–„β–„β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ
> β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€

Kotlin usage:

@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)

Options

type QrOpts = {
  // Default: 'medium'. Low: 7%, medium: 15%, quartile: 25%, high: 30%
  ecc?: 'low' | 'medium' | 'quartile' | 'high';
  // Force specific encoding. Kanji and ECI are not supported yet
  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
};
// - `raw`: 2d boolean array, to use with canvas or other image drawing libraries
// - `ascii`: ASCII symbols, not all fonts will display it properly
// - `term`: terminal color escape sequences. 2x bigger than ASCII, but works with all fonts
// - `gif`: uncompressed gif
// - `svg`: SVG vector image
type Output = 'raw' | 'ascii' | 'term' | 'gif' | 'svg';
function encodeQR(text: string, output: 'raw', opts?: QrOpts): boolean[][];
function encodeQR(text: string, output: 'ascii' | 'term' | 'svg', opts?: QrOpts): string;
function encodeQR(text: string, output: 'gif', opts?: QrOpts): Uint8Array;

Decoding

// gif reader is not included in the package
// but you can decode raw bitmap
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 [email protected]
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 canvas and read back

// Convert SVG to PNG
function svgToPng(svgData, width, height) {
  return new Promise((resolve, reject) => {
    const domparser = new DOMParser();
    const doc = domparser.parseFromString(svgData, 'image/svg+xml');

    const svgElement = doc.documentElement;
    const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect');

    rect.setAttribute('width', '100%');
    rect.setAttribute('height', '100%');
    rect.setAttribute('fill', 'white');
    svgElement.insertBefore(rect, svgElement.firstChild);

    const serializer = new XMLSerializer();
    const source = serializer.serializeToString(doc);

    const img = new Image();
    img.src = 'data:image/svg+xml,' + encodeURIComponent(source);
    img.onload = function () {
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0, width, height);
      const dataUrl = canvas.toDataURL('image/png');
      resolve(dataUrl);
    };
    img.onerror = reject;
  });
}

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 decoding is hard: it is basically computer vision problem. There are two main cases:

  • decoding files. Can be slow, because it is supposed to handle complicated cases such as blur / rotation
  • decoding camera feed. Must be fast; even if one frame fails, next frame can succeed

State-of-the-art is the same as other computer vision problems: neural networks. Using them would make the library hard to audit. Since JS can't access accelerators, it would also likely be very slow. We don't want to use WebGL, it is complex and exposes users to fingerprinting. The implemented reader algorithm is inspired by ZXing:

  1. toBitmap: convert to bitmap, black & white segments. The slowest part and the most important.
  2. detect: find 3 finder patterns and one alignment (for version > 1). This is tricky β€” they can be rotated and distorted by perspective. Square is not really square β€” it's quadrilateral, and we have no idea about its size. The best thing we can do is counting runs of a same color and selecting one which looks like pattern; same almost same ratio of runs.
  3. transform: once patterns have been found, try to fix perspective and transform quadrilateral to square
  4. decodeBitmap: after that, execute encoding in reverse: read information via zig-zag pattern, interleave bytes, correct errors, convert to bits and, finally, read segments from bits to create string.
  5. Finished

Vectors

To test decoding, we use awesome dataset from BoofCV. BoofCV decodes 73% of test cases, zxing decodes 49%. We are almost at parity with zxing (mostly because of ECI stuff not supported). Vectors are preserved in a git repo at github.com/paulmillr/qr-code-vectors.

For testing: accessing camera on iOS Safari requries HTTPS. It means file: protocol or non-encrypted http can't be used.

The spec is available at iso.org for 200 CHF.

Security

There are multiple ways how single text can be encoded:

  • Differences in segmentation: abc123 can become [{type: 'alphanum', 'abc'}, {type: 'num', '123'}], [{type: 'alphanum', 'abc123'}]
  • Differences between mask selection algo in libraries
  • Defaults: error correction level, how many bits are stored before upgrading versions

If an adversary is able to access multiple generated QR codes from a specific library, they can fingerprint a user, which can be then used to exfiltrate data from air-gapped systems. Adversary would then need to create library-specific exploit.

Currently we cross-test against python-qrcode: it is closer to spec than js implementations. We also always use single segment, which is not too optimal, but reduces fingerprinting data.

To improve the behavior, we can cross-test against 3-4 popular libraries.

Speed

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

======== encode/ascii ========
encode/noble 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/noble 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/noble 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/noble 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):  noble=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)

qr's People

Contributors

ardislu avatar mvdschee avatar paulmillr avatar sandstrom avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

qr's Issues

SVG output can't be resized

Currently, the SVG output has a hardcoded physical width and height and no viewBox property specified. Example:

<svg xmlns:svg="http://www.w3.org/2000/svg" width="29" height="29" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <!-- Inner SVG components here -->
</svg>

This makes the SVG element ignore any width or height passed to the element via CSS. For example, this:

<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="29" height="29" version="1.1"
  style="width: 100px; height: 100px;">
  <!-- Inner SVG components here -->
</svg>

will render like this:

image

Instead of hardcoding the physical dimensions, a viewBox width and height should be specified, like this:

<svg xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 29 29" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <!-- Inner SVG components here -->
</svg>

Then the SVG element will scale to any width or height passed to it. For example, this:

<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 29 29" version="1.1"
  style="width: 100px; height: 100px;">
  <!-- Inner SVG components here -->
</svg>

will render like this:

image

readQR error

Node.js v18.16.0

import writeQR from '@paulmillr/qr';
const gifBytes = writeQR('Hello world', 'gif');

import readQR from '@paulmillr/qr/decode.js';
const decoded = readQR({ height: 120, width: 120, data: gifBytes });

Uncaught Error Error: Finder: len(found) = 0
    at findFinder (/Users/xxx/Desktop/test/node_modules/.pnpm/@paulmillr+qr@0.1.1/node_modules/@paulmillr/qr/decode.js:362:15)
    at detect (/Users/xxx/Desktop/test/node_modules/.pnpm/@paulmillr+qr@0.1.1/node_modules/@paulmillr/qr/decode.js:539:28)
    at readQR (/Users/xxx/Desktop/test/node_modules/.pnpm/@paulmillr+qr@0.1.1/node_modules/@paulmillr/qr/decode.js:863:30)
    at <anonymous> (/Users/xxx/Desktop/test/index.js:5:17)
    at run (internal/modules/esm/module_job:194:25)
    --- await ---
    at processTicksAndRejections (internal/process/task_queues:95:5)
    --- await ---
    at runMainESM (internal/modules/run_main:55:21)
    at executeUserEntryPoint (internal/modules/run_main:78:5)
    at <anonymous> (internal/main/run_main_module:23:47)

Kotlin/JS usage in Readme

Spent a bit of time trying to figure out the right way to import into a Kotlin/JS project. This is what it needed to be, might be helpful to have a blurb in the readme:

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

Then it can be used like:

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

Note, this is only neccessary to do manually until the Dukat project rewrite is finished by the Kotlin team.

Decoding Interactive Demo Error

[ERROR] Media loop [object OverconstrainedError]

Chrome Version: "Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.