Skip to content

Commit

Permalink
Merge branch 'pr/15'
Browse files Browse the repository at this point in the history
  • Loading branch information
kaworu committed Dec 3, 2020
2 parents 5dbe92a + 6b6e55a commit ed5a170
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 30 deletions.
18 changes: 17 additions & 1 deletion README.md
Expand Up @@ -16,7 +16,9 @@ and a `constants` object.


## decode(img[, callback])
`img` must be a `Buffer` of a PNG or JPEG encoded image file.
`img` must be either a `Buffer` of a PNG or JPEG encoded image file, or a
decoded image in [`ImageData`](https://developer.mozilla.org/en-US/docs/Web/API/ImageData)
format with 1 (grayscale), 3 (RGB) or 4 (RGBA) channels.

When `callback` is provided, it is expected to be a "classic" Node.js callback
function, taking an error as first argument and the result as second argument.
Expand Down Expand Up @@ -50,6 +52,20 @@ quirc.decode(img).then((codes) => {
}).catch((err) => {
// handle err.
});

// alternatively, use an already-loaded ImageData, e.g. from the `canvas` library
const context = canvas.getContext('2d');
const imageData = context.getImageData(0, 0, 800, 600);
quirc.decode(imageData).then((codes) => {
// do something with codes.
console.log(`codes read from ImageData (size=${
imageData.data.length
}, width=${imageData.width}, height=${
imageData.height
}):`, codes);
}).catch((err) => {
// handle err.
});
```

output:
Expand Down
64 changes: 53 additions & 11 deletions index.js
Expand Up @@ -3,26 +3,68 @@
// Our C++ Addon
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 {
function decodeEncoded(img, callback) {
return addon.decodeEncoded(img, callback);
}

function isImageDimension(number) {
return (
typeof number === 'number' &&
number > 0 &&
(number | 0) === number &&
!isNaN(number)
);
}

function decodeRaw(img, callback) {
if (!isImageDimension(img.width)) {
throw new Error(
`unexpected width value for image: ${img.width}`
)
}
if (!isImageDimension(img.height)) {
throw new Error(
`unexpected height value for image: ${img.height}`
)
}
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`
);
}
return addon.decodeRaw(img.data, img.width, img.height, callback);
}

function maybePromisify(fn) {
return (...args) => {
if (args.length < fn.length) {
return new Promise((resolve, reject) => {
addon.decode(img, (err, results) => {
fn(...args, (err, results) => {
if (err) {
return reject(err);
} else {
return resolve(results);
}
});
});
} else {
return fn(...args);
}
},
};
}

// public API
module.exports = {
decode: maybePromisify((img, callback) => {
if (Buffer.isBuffer(img)) {
return decodeEncoded(img, callback);
} else if (img && typeof img === "object") {
return decodeRaw(img, callback);
} else {
throw new TypeError("img must be a Buffer or ImageData");
}
}),
constants: {
// QR-code versions.
VERSION_MIN: 1,
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.2.1"
},
"contributors": [
Expand Down
48 changes: 42 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 img_width, size_t img_height):
AsyncWorker(callback),
m_img(img),
m_img_len(img_len),
m_img_width(img_width),
m_img_height(img_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_img_width, m_img_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_img_width;
size_t m_img_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,46 @@ 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");
// Uint8ClampedArray is from ImageData#data, Buffer is allowed for convenience.
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 img_width = (size_t)Nan::To<int>(info[1]).FromJust();
size_t img_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, img_width, img_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
57 changes: 51 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 img_width, size_t img_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 img_width, size_t img_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 img_width, size_t img_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, img_width, img_height) == -1) {
// FIXME: more descriptive error here?
list->err = "failed to load image";
goto out;
Expand Down Expand Up @@ -220,15 +221,20 @@ 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 img_width, size_t img_height)
{
if (img_width > 0 && img_height > 0) {
return nq_load_raw(q, img, img_len, img_width, img_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);
}

Expand Down Expand Up @@ -415,3 +421,42 @@ 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 img_width, size_t img_height)
{
if (quirc_resize(q, img_width, img_height) < 0)
goto fail;

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

const size_t len = img_width * img_height;
const int channels = len == img_len ? 1 : /* grayscale */
3 * len == img_len ? 3 : /* rgb */
4 * len == img_len ? 4 : /* rgba */
/* default */ -1;

if (channels == 1) {
memcpy(image, img, img_len);
} else if (channels == 3 || channels == 4) {
for (
size_t dst_offset = 0, src_offset = 0;
dst_offset < img_width * img_height;
dst_offset++, src_offset += channels
) {
uint8_t r = img[src_offset];
uint8_t g = img[src_offset + 1];
uint8_t b = img[src_offset + 2];
// convert RGB to grayscale, ignoring alpha channel if present, using this:
// https://en.wikipedia.org/wiki/Grayscale#Colorimetric_(perceptual_luminance-preserving)_conversion_to_grayscale
image[dst_offset] = (uint8_t)(0.2126 * (float)r + 0.7152 * (float)g + 0.0722 * (float)b);
}
} 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
40 changes: 35 additions & 5 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 @@ -92,10 +93,13 @@ describe("constants", function () {

describe("decode()", function () {
describe("arguments", function () {
it("should throw an Error when no arguments are given", function () {
expect(function () {
quirc.decode();
}).to.throw(Error, "img must be a Buffer");
it("should return a rejected Promise when no arguments are given", function (done) {
const p = quirc.decode()
expect(p).to.be.a("Promise");
p.catch((e) => {
expect(e.message).to.eql("img must be a Buffer or ImageData");
done();
});
});
it("should return a Promise when only one argument is given", function () {
const p = quirc.decode(Buffer.from("data"));
Expand All @@ -105,7 +109,7 @@ describe("decode()", function () {
it("should throw when img is not a Buffer", function () {
expect(function () {
quirc.decode("a string", function dummy() { });
}).to.throw(TypeError, "img must be a Buffer");
}).to.throw(TypeError, "img must be a Buffer or ImageData");
});
it("should throw when callback is not a function", function () {
expect(function () {
Expand Down Expand Up @@ -357,6 +361,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 ed5a170

Please sign in to comment.