Skip to content

Commit

Permalink
feat: support reading QR codes from ImageData
Browse files Browse the repository at this point in the history
Rather than requiring that the data be in PNG or JPEG format, allow it to be an ImageData object with raw pixel data. This is useful if, for example, you are looking for a QR code in a portion of an image you've already loaded and cropped.
  • Loading branch information
eventualbuddha committed Nov 11, 2020
1 parent ed7c9b7 commit d066972
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 27 deletions.
9 changes: 9 additions & 0 deletions README.md
Expand Up @@ -50,6 +50,15 @@ quirc.decode(img).then((codes) => {
}).catch((err) => {
// handle err.
});

// alternatively, use an already-loaded ImageData, e.g. from a Canvas element
const context = canvas.getContext('2d');
const imageData = context.getImageData(0, 0, 800, 600);
quirc.decode(imageData).then((codes) => {
// do something with codes.
}).catch((err) => {
// handle err.
});
```

output:
Expand Down
50 changes: 36 additions & 14 deletions index.js
Expand Up @@ -6,21 +6,43 @@ const addon = require('bindings')('node-quirc.node');
// public API
module.exports = {
decode(img, callback) {
if (!Buffer.isBuffer(img)) {
throw new TypeError('img must be a Buffer');
}
if (callback) {
return addon.decode(img, callback);
} else {
return new Promise((resolve, reject) => {
addon.decode(img, (err, results) => {
if (err) {
return reject(err);
} else {
return resolve(results);
}
if (Buffer.isBuffer(img)) {
if (callback) {
return addon.decodeEncoded(img, callback);
} else {
return new Promise((resolve, reject) => {
addon.decodeEncoded(img, (err, results) => {
if (err) {
return reject(err);
} else {
return resolve(results);
}
});
});
});
}
} else if (img && typeof img === "object") {
const channels = img.data.length / img.width / img.height;
if (channels !== 1 && channels !== 3 && channels !== 4) {
throw new Error(
`unsupported ${channels}-channel image, expected 1, 3, or 4`
);
}

if (callback) {
addon.decodeRaw(img.data, img.width, img.height, callback);
} else {
return new Promise((resolve, reject) => {
addon.decodeRaw(img.data, img.width, img.height, (err, results) => {
if (err) {
return reject(err);
} else {
return resolve(results);
}
});
});
}
} else {
throw new TypeError("img must be a Buffer or ImageData");
}
},
constants: {
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -36,6 +36,7 @@
},
"devDependencies": {
"chai": "^4.2.0",
"jpeg-js": "^0.4.0",
"mocha": "^8.1.3"
},
"contributors": [
Expand Down
47 changes: 41 additions & 6 deletions src/node-quirc.cc
Expand Up @@ -26,10 +26,12 @@ class NodeQuircDecoder: public AsyncWorker
public:

/* ctor */
NodeQuircDecoder(Callback *callback, const uint8_t *img, size_t img_len):
NodeQuircDecoder(Callback *callback, const uint8_t *img, size_t img_len, size_t width, size_t height):
AsyncWorker(callback),
m_img(img),
m_img_len(img_len),
m_width(width),
m_height(height),
m_code_list(NULL)
{ }

Expand All @@ -46,7 +48,7 @@ class NodeQuircDecoder: public AsyncWorker
// everything we need for input and output should go on `this`.
void Execute()
{
m_code_list = nq_decode(m_img, m_img_len);
m_code_list = nq_decode(m_img, m_img_len, m_width, m_height);
}


Expand Down Expand Up @@ -87,6 +89,8 @@ class NodeQuircDecoder: public AsyncWorker
/* nq_decode() arguments */
const uint8_t *m_img;
size_t m_img_len;
size_t m_width;
size_t m_height;
/* nq_decode() return value */
struct nq_code_list *m_code_list;

Expand Down Expand Up @@ -133,7 +137,7 @@ class NodeQuircDecoder: public AsyncWorker


// async access to nq_decode()
NAN_METHOD(NodeQuircDecodeAsync) {
NAN_METHOD(NodeQuircDecodeEncodedAsync) {
if (info.Length() < 2)
return ThrowError("expected (img, callback) as arguments");
if (!node::Buffer::HasInstance(info[0]))
Expand All @@ -144,14 +148,45 @@ NAN_METHOD(NodeQuircDecodeAsync) {
uint8_t *img = (uint8_t *)node::Buffer::Data(info[0]);
size_t img_len = node::Buffer::Length(info[0]);
Callback *callback = new Callback(info[1].As<v8::Function>());
AsyncQueueWorker(new NodeQuircDecoder(callback, img, img_len));
AsyncQueueWorker(new NodeQuircDecoder(callback, img, img_len, 0, 0));
}

NAN_METHOD(NodeQuircDecodeRawAsync) {
if (info.Length() < 4)
return ThrowError("expected (pixels, width, height, callback) as arguments");
if (!info[0]->IsUint8ClampedArray() && !node::Buffer::HasInstance(info[0]))
return ThrowTypeError("pixels must be a Uint8ClampedArray or Buffer");
if (!info[1]->IsNumber())
return ThrowTypeError("width must be a number");
if (!info[2]->IsNumber())
return ThrowTypeError("height must be a number");
if (!info[3]->IsFunction())
return ThrowTypeError("callback must be a function");

uint8_t *img;
size_t img_len;

if (node::Buffer::HasInstance(info[0])) {
img = (uint8_t *)node::Buffer::Data(info[0]);
img_len = node::Buffer::Length(info[0]);
} else {
Nan::TypedArrayContents<uint8_t> data(info[0]);
img = *data;
img_len = data.length();
}

size_t width = (size_t)Nan::To<int>(info[1]).FromJust();
size_t height = (size_t)Nan::To<int>(info[2]).FromJust();
Callback *callback = new Callback(info[3].As<v8::Function>());
AsyncQueueWorker(new NodeQuircDecoder(callback, img, img_len, width, height));
}

// export stuff to NodeJS
NAN_MODULE_INIT(NodeQuircInit) {
Set(target, New("decode").ToLocalChecked(),
GetFunction(New<v8::FunctionTemplate>(NodeQuircDecodeAsync)).ToLocalChecked());
Set(target, New("decodeEncoded").ToLocalChecked(),
GetFunction(New<v8::FunctionTemplate>(NodeQuircDecodeEncodedAsync)).ToLocalChecked());
Set(target, New("decodeRaw").ToLocalChecked(),
GetFunction(New<v8::FunctionTemplate>(NodeQuircDecodeRawAsync)).ToLocalChecked());
}


Expand Down
64 changes: 58 additions & 6 deletions src/node_quirc_decode.c
Expand Up @@ -27,13 +27,14 @@ struct nq_code {
struct quirc_data qdata;
};

static int nq_load_image(struct quirc *q, const uint8_t *img, size_t img_len);
static int nq_load_image(struct quirc *q, const uint8_t *img, size_t img_len, size_t width, size_t height);
static int nq_load_png(struct quirc *q, const uint8_t *img, size_t img_len);
static int nq_load_jpeg(struct quirc *q, const uint8_t *img, size_t img_len);
static int nq_load_raw(struct quirc *q, const uint8_t *img, size_t img_len, size_t width, size_t height);


struct nq_code_list *
nq_decode(const uint8_t *img, size_t img_len)
nq_decode(const uint8_t *img, size_t img_len, size_t width, size_t height)
{
struct nq_code_list *list = NULL;
struct quirc *q = NULL;
Expand All @@ -48,7 +49,7 @@ nq_decode(const uint8_t *img, size_t img_len)
goto out;
}

if (nq_load_image(q, img, img_len) == -1) {
if (nq_load_image(q, img, img_len, width, height) == -1) {
// FIXME: more descriptive error here?
list->err = "failed to load image";
goto out;
Expand Down Expand Up @@ -220,18 +221,23 @@ nq_code_payload_len(const struct nq_code *code)

/* returns 0 on success, -1 on error */
static int
nq_load_image(struct quirc *q, const uint8_t *img, size_t img_len)
nq_load_image(struct quirc *q, const uint8_t *img, size_t img_len, size_t width, size_t height)
{
int ret = -1; /* error */

/* NOTE: only png is supported at the moment */
if (img_len >= PNG_BYTES_TO_CHECK) {
if (png_sig_cmp((uint8_t *)img, (png_size_t)0, PNG_BYTES_TO_CHECK) == 0)
ret = nq_load_png(q, img, img_len);
else
}

if (ret != 0) {
ret = nq_load_jpeg(q, img, img_len);
}

if (ret != 0 && width != 0 && height != 0) {
ret = nq_load_raw(q, img, img_len, width, height);
}

return (ret);
}

Expand Down Expand Up @@ -415,3 +421,49 @@ nq_load_jpeg(struct quirc *q, const uint8_t *img, size_t img_len)
jpeg_destroy_decompress(&dinfo);
return -1;
}

static int
nq_load_raw(struct quirc *q, const uint8_t *img, size_t img_len, size_t width, size_t height)
{
if (quirc_resize(q, width, height) < 0)
goto fail;

uint8_t *image = quirc_begin(q, NULL, NULL);

int channels = img_len / width / height;

if (channels == 1)
{
memcpy(image, img, img_len);
}
else if (channels == 3 || channels == 4)
{
size_t dst_offset = width * height - 1;
size_t src_offset = dst_offset * channels;
do
{
uint8_t r = img[src_offset];
uint8_t g = img[src_offset + 1];
uint8_t b = img[src_offset + 2];
// ignore alpha, if present
image[dst_offset] = (uint8_t)(0.2126 * (float)r + 0.7152 * (float)g + 0.0722 * (float)b);

if (dst_offset == 0)
{
break;
}

src_offset -= channels;
dst_offset--;
} while (1);
}
else
{
goto fail;
}

return 0;

fail:
return -1;
}
2 changes: 1 addition & 1 deletion src/node_quirc_decode.h
Expand Up @@ -10,7 +10,7 @@
struct nq_code_list;
struct nq_code;

struct nq_code_list *nq_decode(const uint8_t *img, size_t imglen);
struct nq_code_list *nq_decode(const uint8_t *img, size_t img_len, size_t width, size_t height);
const char *nq_code_list_err(const struct nq_code_list *list);
unsigned int nq_code_list_size(const struct nq_code_list *list);
const struct nq_code *nq_code_at(const struct nq_code_list *list, unsigned int index);
Expand Down
27 changes: 27 additions & 0 deletions test/index.js
Expand Up @@ -3,6 +3,7 @@
const fs = require("fs");
const path = require("path");
const util = require("util");
const jpeg = require("jpeg-js");

const chai = require("chai");
const expect = chai.expect;
Expand Down Expand Up @@ -357,6 +358,32 @@ describe("decode()", function () {
}
});

context("raw image data", function () {
it("should read QR codes from raw image data", function (done) {
const big_image_with_two_qrcodes = jpeg.decode(
read_test_data("big_image_with_two_qrcodes.jpeg")
);
quirc.decode(big_image_with_two_qrcodes, function (err, codes) {
expect(codes).to.be.an("array").and.to.have.length(2);
expect(codes[0].err).to.not.exist;
expect(codes[0].version).to.eql(4);
expect(codes[0].ecc_level).to.eql("M");
expect(codes[0].mask).to.eql(2);
expect(codes[0].mode).to.eql("BYTE");
expect(codes[0].data).to.be.an.instanceof(Buffer);
expect(codes[0].data.toString()).to.eql("from javascript");
expect(codes[1].err).to.not.exist;
expect(codes[1].version).to.eql(4);
expect(codes[1].ecc_level).to.eql("M");
expect(codes[1].mask).to.eql(2);
expect(codes[1].mode).to.eql("BYTE");
expect(codes[1].data).to.be.an.instanceof(Buffer);
expect(codes[1].data.toString()).to.eql("here comes qr!");
done();
});
});
});

context("regressions", function () {
// https://github.com/dlbeer/quirc/pull/87
context("dark image", function () {
Expand Down

0 comments on commit d066972

Please sign in to comment.