From 0b43aee9299ed06a890e3667114e64926f84764a Mon Sep 17 00:00:00 2001 From: Brian J Brennan Date: Thu, 7 Mar 2013 20:54:59 -0500 Subject: [PATCH] Add `bin/oven`, update readme, change function name. --- README.md | 55 +++++++++++++++++++++++++++++++++--- bin/oven | 57 ++++++++++++++++++++++++++++++++++++++ lib/bakery.js => index.js | 38 +++++++++++++------------ package.json | 6 ++-- test/bakery.test.js | 6 ++-- test/unbaked.png | Bin 0 -> 10135 bytes 6 files changed, 135 insertions(+), 27 deletions(-) create mode 100755 bin/oven rename lib/bakery.js => index.js (76%) create mode 100644 test/unbaked.png diff --git a/README.md b/README.md index 4b56fc2..cb77f67 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,55 @@ # openbadges-bakery [![Build Status](https://secure.travis-ci.org/mozilla/openbadges-bakery.png)](http://travis-ci.org/mozilla/openbadges-bakery) -Provides two methods: +# Install +```bash +$ npm install openbadges-bakery +``` +# CLI Usage + +## Baking + +```bash +$ oven [--in path/to/image.png] [--out path/to/baked-image.png] +``` +If `--out` is not set, the baked image will print to stdout. + +The input file can also be piped into stdin. -```js -bakery.bake(buffer | stream, callback); -bakery.debake(buffer | stream, callback); +```bash +$ cat path/to/image.png | oven > path/to/baked-image.png ``` +## Extracting + +```bash +$ oven [--in path/to/image.png] --extract +``` + +Same as above, you can also pipe a file to stdin. The data will be printed to stdout. + +# Libary Usage + +## bakery.bake(options callback); + +Bakes some data into an image. + +Options are +- `image`: either a buffer or a stream representing the PNG to bake +- `data`: the data to put into the badge. At this point, it should likely be a URL pointing to a badge assertion + +`callback` has the signature `function(err, imageData)` + +## bakery.debake(image, callback); + +Gets the URL from a baked badge and attempts to retreive the assertion +at the other end. + +`image` should be a stream or a buffer + +`callback` has the signature `function (err, object)` where `object` is expected to be a OpenBadges assertion. + +## bakery.extract(image, callback) + +Gets the data from the baked badge. + +`callback` has the signature `function (err, data)` + diff --git a/bin/oven b/bin/oven new file mode 100755 index 0000000..9ee8c66 --- /dev/null +++ b/bin/oven @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const argv = require('optimist').argv; +const stdout = process.stdout; +const stdin = process.stdin; +const stderr = process.stderr; +const bakery = require('../'); +const path = require('path'); +const url = argv._[0]; +const infile = argv.in||argv.infile; +const outfile = argv.out||argv.outfile; +const extract = argv.extract; + +function log(msg) { + stderr.write(msg + '\n'); +} + +if (!url && !extract) { + log('You must pass a url to bake into the image'); + process.exit(1); +} + +var stream; +if (infile) { + stream = fs.createReadStream(path.resolve(infile)); +} else { + stdin.resume(); + stream = stdin; +} + +if (extract) { + bakery.extract(stream, function done(err, data) { + if (err) { + log('there was an error:', err.message); + process.exit(1); + } + stdout.write(data + '\n'); + process.exit(1); + }) +} + +bakery.bake({ + image: stream, + url: url +}, function done(err, baked) { + if (err) { + log('there was an error:', err.message); + process.exit(1); + } + if (outfile) { + fs.writeFileSync(path.resolve(outfile), baked) + } else { + stdout.write(baked); + } + process.exit(0); +}); diff --git a/lib/bakery.js b/index.js similarity index 76% rename from lib/bakery.js rename to index.js index 699d9fd..80c3bc2 100644 --- a/lib/bakery.js +++ b/index.js @@ -6,12 +6,12 @@ * Licensed under the MPL 2.0 license. */ -var util = require('util'); -var streampng = require('streampng'); -var request = require('request'); -var urlutil = require('url'); +const util = require('util'); +const streampng = require('streampng'); +const request = require('request'); +const urlutil = require('url'); -var KEYWORD = 'openbadges'; +const KEYWORD = 'openbadges'; function createChunk(url) { return streampng.Chunk.tEXt({ @@ -21,11 +21,13 @@ function createChunk(url) { } exports.bake = function bake(options, callback) { - var buffer = options.image; - var png = streampng(buffer); - // #TODO: make sure the url is set - var chunk = createChunk(options.url); + const buffer = options.image; + const data = options.url || options.data; + const png = streampng(buffer); + const chunk = createChunk(data); var existingChunk; + if (!data) + return callback(new Error('must pass a `data` or `url` option')); png.inject(chunk, function (txtChunk) { if (txtChunk.keyword === KEYWORD) { existingChunk = txtChunk; @@ -33,8 +35,8 @@ exports.bake = function bake(options, callback) { } }); if (existingChunk) { - var msg = util.format('This image already has a chunk with the `%s` keyword (contains: %j)', KEYWORD, chunk.text); - var error = new Error(msg); + const msg = util.format('This image already has a chunk with the `%s` keyword (contains: %j)', KEYWORD, chunk.text); + const error = new Error(msg); error.code = 'IMAGE_ALREADY_BAKED'; error.contents = existingChunk.text; return callback(error); @@ -42,8 +44,8 @@ exports.bake = function bake(options, callback) { return png.out(callback); }; -exports.getBakedData = function getBakedData(img, callback) { - var png = streampng(img); +exports.extract = function extract(img, callback) { + const png = streampng(img); var found = false; function textListener(chunk) { @@ -56,7 +58,7 @@ exports.getBakedData = function getBakedData(img, callback) { function endListener() { if (!found) { - var error = new Error('Image does not have any baked in data.'); + const error = new Error('Image does not have any baked in data.'); error.code = 'IMAGE_UNBAKED'; return callback(error); } @@ -68,7 +70,7 @@ exports.getBakedData = function getBakedData(img, callback) { exports.debake = function debake(image, callback) { - exports.getBakedData(image, function (error, url) { + exports.extract(image, function (error, url) { if (error) return callback(error); @@ -78,8 +80,8 @@ exports.debake = function debake(image, callback) { return callback(error); } - var status = response.statusCode; - var type = response.headers['content-type']; + const status = response.statusCode; + const type = response.headers['content-type']; if (status == 200 && type == 'application/json') return exports.parseResponse(body, url, callback); @@ -104,7 +106,7 @@ exports.parseResponse = function parseResponse(body, url, callback) { return callback(null, obj); }; -var errors = { +const errors = { request: function (original, url) { var msg = util.format('There was an error initiating the request: %s', original.message); var error = new Error(msg); diff --git a/package.json b/package.json index d138268..7b06cd3 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,15 @@ "name": "openbadges-bakery", "version": "0.1.1", "description": "Tools for baking and debaking openbadge images", - "main": "lib/bakery.js", + "main": "index.js", "directories": { "test": "test" }, + "bin": "bin/oven", "dependencies": { "request": "~2.11.4", - "streampng": "~0.1.1" + "streampng": "~0.1.1", + "optimist": "~0.3.5" }, "devDependencies": { "oneshot": "~0.1.0", diff --git a/test/bakery.test.js b/test/bakery.test.js index 5c7c99a..718c780 100644 --- a/test/bakery.test.js +++ b/test/bakery.test.js @@ -5,7 +5,7 @@ var pathutil = require('path'); var urlutil = require('url'); var oneshot = require('oneshot'); -var bakery = require('../lib/bakery'); +var bakery = require('..'); var IMG_PATH = pathutil.join(__dirname, 'testimage.png'); var ASSERTION_URL = "http://example.org"; @@ -70,7 +70,7 @@ test('bakery.bake: takes a buffer', function (t) { bakery.bake(options, function (err, baked) { t.notOk(err, 'should not have an error'); t.ok(baked, 'should get back some data'); - bakery.getBakedData(baked, function (err, url) { + bakery.extract(baked, function (err, url) { t.notOk(err, 'there should be a matching tEXt chunk'); t.same(url, ASSERTION_URL, 'should be able to find the url'); t.end(); @@ -87,7 +87,7 @@ test('bakery.bake: takes a stream', function (t) { bakery.bake(options, function (err, baked) { t.notOk(err, 'should not have an error'); t.ok(baked, 'should get back some data'); - bakery.getBakedData(baked, function (err, url) { + bakery.extract(baked, function (err, url) { t.notOk(err, 'there should be a matching tEXt chunk'); t.same(url, ASSERTION_URL, 'should be able to find the url'); t.end(); diff --git a/test/unbaked.png b/test/unbaked.png new file mode 100644 index 0000000000000000000000000000000000000000..d181cc02cfef94a836ddebe8f98e8467ae4e3b00 GIT binary patch literal 10135 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

anBY4_~X4atB z670Ee_Nq#w8`a_@2YTQH8wf)Pf5cvXzuJn&hg#QRsEQEQkVWlp-njgs zYFVl_Fl++@`uOxYhgYQj^r*bJSUiP;4SPlz^$9BvupS$3!xoqg#U#uFQ+qEc@ZXi63&mXXAT>q$M_p<)4c@h3mPgbYXE>~n7tX2}`d z-LKN>p>)S@$qN@yd8T%DpL+^xvj>HKJRn9&xm2Nf(eXm7+v3jFf;IhUJ&feGP`$Lm ziNa-L7}^5_(Ael@St|q_aUVUy5BtnbC+QTGr5&)w3&)rd|465D9GY53(z{qGE1`*A zrZ5pZNTSB`k|R**TA!$J;XS^|1P&n{9n4e`jDq0!T`W3EsrYyWLHSVew7atww&`Ai5?gP=%HBx0Wrz8Wm8^-y;7>NSCZ7 zZp0b@G-YAJ0ictRE~pRIMSx1m#xb}%t>nkic9Lh^t;s3T>yUUno6F2<0o`oa-GveH zd^RTun-J$H4^uuHw6w|j<(EPqG3dzWdd4M|0B5z3b&|0bdYEG>8wc!7jemx7RPDUE9e&Uo{T_jVc4I>~2 z&a$F{nD$oTkb@~ literal 0 HcmV?d00001