Skip to content

Commit

Permalink
Add Cloud Functions ImageMagick sample.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmdobry committed Apr 25, 2017
1 parent ce94259 commit b0b0cb8
Show file tree
Hide file tree
Showing 7 changed files with 4,548 additions and 4 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ on Google Cloud Platform.
* [Google App Engine (Flexible Environment)](#google-app-engine-flexible-environment)
* [Google Compute Engine](#google-compute-engine)
* [Google Container Engine](#google-container-engine)
* [Google Cloud Functions (Alpha)](#google-cloud-functions-alpha)
* [Google Cloud Functions (Beta)](#google-cloud-functions-beta)
* [**Storage and Databases**](#storage-and-databases)
* [Google Cloud Datastore](#google-cloud-datastore)
* [Google Cloud Storage](#google-cloud-storage)
Expand Down Expand Up @@ -207,9 +207,7 @@ View the [Container Engine Node.js samples][container_samples].
[container_docs]: https://cloud.google.com/container-engine/docs/
[container_samples]: containerengine

#### Google Cloud Functions (Alpha)

[Sign up for the Alpha][functions_signup].
#### Google Cloud Functions (Beta)

[Cloud Functions][functions_docs] is a lightweight, event-based, asynchronous
compute solution that allows you to create small, single-purpose functions that
Expand Down
1 change: 1 addition & 0 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ dependencies:
- functions/gcs
- functions/helloworld
- functions/http
- functions/imagemagick
- functions/log
- functions/ocr/app
- functions/pubsub
Expand Down
55 changes: 55 additions & 0 deletions functions/imagemagick/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<img src="https://avatars2.githubusercontent.com/u/2810941?v=3&s=96" alt="Google Cloud Platform logo" title="Google Cloud Platform" align="right" height="96" width="96"/>

# Google Cloud Functions ImageMagick sample

This sample shows you how to blur an image using ImageMagick in a
Storage-triggered Cloud Function.

View the [source code][code].

[code]: index.js

## Deploy and Test

1. Follow the [Cloud Functions quickstart guide][quickstart] to setup Cloud
Functions for your project.

1. Clone this repository:

git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git
cd nodejs-docs-samples/functions/imagemagick

1. Create a Cloud Storage bucket for storing images (if you already have one you
want to use, you can skip this step):

gsutil mb gs://YOUR_BUCKET_NAME

* Replace `YOUR_BUCKET_NAME` with the name of your image Bucket.

1. Create a Cloud Storage Bucket to stage our deployment:

gsutil mb gs://YOUR_STAGE_BUCKET_NAME

* Replace `YOUR_STAGE_BUCKET_NAME` with the name of your Cloud Storage Bucket.

1. Deploy the `blurOffensiveImages` function with a Storage trigger:

gcloud alpha functions deploy blurOffensiveImages --trigger-bucket=YOUR_BUCKET_NAME --stage-bucket=YOUR_STAGE_BUCKET_NAME

* Replace `YOUR_BUCKET_NAME` with the name of your image Cloud Storage Bucket.
* Replace `YOUR_STAGE_BUCKET_NAME` with the name of your Cloud Storage Bucket.

1. Upload an offensive image to your image Storage bucket, such as this image of
a flesh-eating zombie: https://cdn.pixabay.com/photo/2015/09/21/14/24/zombie-949916_1280.jpg

1. Check the logs for the `blurOffensiveImages` function:

gcloud beta functions get-logs blurOffensiveImages

You should see something like this in your console:

D ... User function triggered, starting execution
I ... `The image zombie.jpg has been detected as inappropriate.`
D ... Execution took 1 ms, user function completed successfully

[quickstart]: https://cloud.google.com/functions/quickstart
105 changes: 105 additions & 0 deletions functions/imagemagick/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright 2017, Google, Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

const exec = require('child_process').exec;
const fs = require('fs');
const path = require('path');
const storage = require('@google-cloud/storage')();
const vision = require('@google-cloud/vision')();

// Blurs uploaded images that are flagged as Adult or Violence.
exports.blurOffensiveImages = (event) => {
const object = event.data;

// Exit if this is a deletion or a deploy event.
if (object.resourceState === 'not_exists') {
console.log('This is a deletion event.');
return;
} else if (!object.name) {
console.log('This is a deploy event.');
return;
}

const file = storage.bucket(object.bucket).file(object.name);

console.log(`Analyzing ${file.name}.`);

return vision.detectSafeSearch(file)
.catch((err) => {
console.error(`Failed to analyze ${file.name}.`, err);
return Promise.reject(err);
})
.then(([safeSearch]) => {
if (safeSearch.adult || safeSearch.violence) {
console.log(`The image ${file.name} has been detected as inappropriate.`);
return blurImage(file, safeSearch);
} else {
console.log(`The image ${file.name} has been detected as OK.`);
}
});
};

// Blurs the given file using ImageMagick.
function blurImage (file) {
const tempLocalFilename = `/tmp/${path.parse(file.name).base}`;

// Download file from bucket.
return file.download({ destination: tempLocalFilename })
.catch((err) => {
console.error('Failed to download file.', err);
return Promise.reject(err);
})
.then(() => {
console.log(`Image ${file.name} has been downloaded to ${tempLocalFilename}.`);

// Blur the image using ImageMagick.
return new Promise((resolve, reject) => {
exec(`convert ${tempLocalFilename} -channel RGBA -blur 0x24 ${tempLocalFilename}`, { stdio: 'ignore' }, (err, stdout) => {
if (err) {
console.error('Failed to blur image.', err);
reject(err);
} else {
resolve(stdout);
}
});
});
})
.then(() => {
console.log(`Image ${file.name} has been blurred.`);

// Upload the Blurred image back into the bucket.
return file.bucket.upload(tempLocalFilename, { destination: file.name })
.catch((err) => {
console.error('Failed to upload blurred image.', err);
return Promise.reject(err);
});
})
.then(() => {
console.log(`Blurred image has been uploaded to ${file.name}.`);

// Delete the temporary file.
return new Promise((resolve, reject) => {
fs.unlink(tempLocalFilename, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
});
}
33 changes: 33 additions & 0 deletions functions/imagemagick/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "nodejs-docs-samples-functions-imagemagick",
"version": "0.0.1",
"private": true,
"license": "Apache-2.0",
"author": "Google Inc.",
"repository": {
"type": "git",
"url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
},
"cloud": {
"requiresKeyFile": true,
"requiresProjectId": true
},
"engines": {
"node": ">=4.3.2"
},
"scripts": {
"lint": "samples lint",
"pretest": "npm run lint",
"test": "ava -T 20s --verbose test/*.test.js"
},
"dependencies": {
"@google-cloud/storage": "1.1.0",
"@google-cloud/vision": "0.11.2"
},
"devDependencies": {
"@google-cloud/nodejs-repo-tools": "1.3.1",
"ava": "0.19.1",
"proxyquire": "1.7.11",
"sinon": "2.1.0"
}
}
105 changes: 105 additions & 0 deletions functions/imagemagick/test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright 2017, Google, Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

const proxyquire = require(`proxyquire`).noCallThru();
const sinon = require(`sinon`);
const test = require(`ava`);
const tools = require(`@google-cloud/nodejs-repo-tools`);

const bucketName = `my-bucket`;
const filename = `image.jpg`;

function getSample () {
const file = {
getMetadata: sinon.stub().returns(Promise.resolve([{}])),
setMetadata: sinon.stub().returns(Promise.resolve()),
download: sinon.stub().returns(Promise.resolve()),
bucket: bucketName,
name: filename
};
const bucket = {
file: sinon.stub().returns(file),
upload: sinon.stub().returns(Promise.resolve())
};
file.bucket = bucket;
const storageMock = {
bucket: sinon.stub().returns(bucket)
};
const visionMock = {
detectSafeSearch: sinon.stub().returns(Promise.resolve([{ violence: true }]))
};
const StorageMock = sinon.stub().returns(storageMock);
const VisionMock = sinon.stub().returns(visionMock);
const childProcessMock = {
exec: sinon.stub().yields()
};
const fsMock = {
unlink: sinon.stub().yields()
};

return {
program: proxyquire(`../`, {
'@google-cloud/vision': VisionMock,
'@google-cloud/storage': StorageMock,
'child_process': childProcessMock,
'fs': fsMock
}),
mocks: {
fs: fsMock,
childProcess: childProcessMock,
storage: storageMock,
bucket,
file,
vision: visionMock
}
};
}

test.beforeEach(tools.stubConsole);
test.afterEach.always(tools.restoreConsole);

test.serial(`blurOffensiveImages does nothing on delete`, async (t) => {
await getSample().program.blurOffensiveImages({ data: { resourceState: `not_exists` } });
t.is(console.log.callCount, 1);
t.deepEqual(console.log.getCall(0).args, ['This is a deletion event.']);
});

test.serial(`blurOffensiveImages does nothing on deploy`, async (t) => {
await getSample().program.blurOffensiveImages({ data: {} });
t.is(console.log.callCount, 1);
t.deepEqual(console.log.getCall(0).args, ['This is a deploy event.']);
});

test.serial(`blurOffensiveImages blurs images`, async (t) => {
const sample = getSample();
await sample.program.blurOffensiveImages({ data: { bucket: bucketName, name: filename } });
t.is(console.log.callCount, 5);
t.deepEqual(console.log.getCall(0).args, [`Analyzing ${sample.mocks.file.name}.`]);
t.deepEqual(console.log.getCall(1).args, [`The image ${sample.mocks.file.name} has been detected as inappropriate.`]);
t.deepEqual(console.log.getCall(2).args, [`Image ${sample.mocks.file.name} has been downloaded to /tmp/${sample.mocks.file.name}.`]);
t.deepEqual(console.log.getCall(3).args, [`Image ${sample.mocks.file.name} has been blurred.`]);
t.deepEqual(console.log.getCall(4).args, [`Blurred image has been uploaded to ${sample.mocks.file.name}.`]);
});

test.serial(`blurOffensiveImages ignores safe images`, async (t) => {
const sample = getSample();
sample.mocks.vision.detectSafeSearch = sinon.stub().returns(Promise.resolve([{}]));
await sample.program.blurOffensiveImages({ data: { bucket: bucketName, name: filename } });
t.is(console.log.callCount, 2);
t.deepEqual(console.log.getCall(0).args, [`Analyzing ${sample.mocks.file.name}.`]);
t.deepEqual(console.log.getCall(1).args, [`The image ${sample.mocks.file.name} has been detected as OK.`]);
});
Loading

0 comments on commit b0b0cb8

Please sign in to comment.