Skip to content
Merged
4 changes: 2 additions & 2 deletions functions/imagemagick/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<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
# Google Cloud Functions imagemagick sample

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

View the [source code][code].
Expand Down
42 changes: 23 additions & 19 deletions functions/imagemagick/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
'use strict';

// [START functions_imagemagick_setup]
const gm = require('gm').subClass({imageMagick: true});
const sharp = require('sharp');
const fs = require('fs').promises;
const path = require('path');
const vision = require('@google-cloud/vision');
Expand All @@ -32,6 +32,12 @@ const {BLURRED_BUCKET_NAME} = process.env;
exports.blurOffensiveImages = async event => {
// This event represents the triggering Cloud Storage object.
const object = event;
if (object.bucket === BLURRED_BUCKET_NAME) {
console.log(
'Event triggered by the blurred bucket; skip to avoid recursion'
);
return;
}

const file = storage.bucket(object.bucket).file(object.name);
const filePath = `gs://${object.bucket}/${object.name}`;
Expand Down Expand Up @@ -60,9 +66,10 @@ exports.blurOffensiveImages = async event => {
// [END functions_imagemagick_analyze]

// [START functions_imagemagick_blur]
// Blurs the given file using ImageMagick, and uploads it to another bucket.
// Blurs the given file using sharp, and uploads it to another bucket.
const blurImage = async (file, blurredBucketName) => {
const tempLocalPath = `/tmp/${path.parse(file.name).base}`;
const tempLocalBlurredPath = `/tmp/blurred-${path.parse(file.name).base}`;

// Download file from bucket.
try {
Expand All @@ -72,34 +79,31 @@ const blurImage = async (file, blurredBucketName) => {
} catch (err) {
throw new Error(`File download failed: ${err}`);
}
try {
await sharp(tempLocalPath).blur(16).toFile(tempLocalBlurredPath);

await new Promise((resolve, reject) => {
gm(tempLocalPath)
.blur(0, 16)
.write(tempLocalPath, (err, stdout) => {
if (err) {
console.error('Failed to blur image.', err);
reject(err);
} else {
console.log(`Blurred image: ${file.name}`);
resolve(stdout);
}
});
});
console.log(`Blurred image: ${file.name}`);
} catch (err) {
console.error('Failed to blur image.', err);
throw err;
}

// Upload result to a different bucket, to avoid re-triggering this function.
const blurredBucket = storage.bucket(blurredBucketName);

// Upload the Blurred image back into the bucket.
const gcsPath = `gs://${blurredBucketName}/${file.name}`;
try {
await blurredBucket.upload(tempLocalPath, {destination: file.name});
await blurredBucket.upload(tempLocalBlurredPath, {destination: file.name});
console.log(`Uploaded blurred image to: ${gcsPath}`);
} catch (err) {
throw new Error(`Unable to upload blurred image to ${gcsPath}: ${err}`);
} finally {
// Delete the temporary file.
await Promise.allSettled([
fs.unlink(tempLocalPath),
fs.unlink(tempLocalBlurredPath),
]);
}

// Delete the temporary file.
return fs.unlink(tempLocalPath);
};
// [END functions_imagemagick_blur]
4 changes: 2 additions & 2 deletions functions/imagemagick/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
"url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
},
"engines": {
"node": ">=12.0.0"
"node": ">=18.17.0"
},
"scripts": {
"test": "c8 mocha -p -j 2 test/*.test.js --timeout=30000 --exit"
},
"dependencies": {
"@google-cloud/storage": "^7.0.0",
"@google-cloud/vision": "^4.0.0",
"gm": "^1.23.1"
"sharp": "^0.34.5"
Comment thread
angelcaamal marked this conversation as resolved.
},
"devDependencies": {
"@google-cloud/functions-framework": "^3.0.0",
Expand Down
69 changes: 39 additions & 30 deletions functions/imagemagick/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
'use strict';

const assert = require('assert');
const {execSync, spawn} = require('child_process');
const {spawn} = require('child_process');
const {Storage} = require('@google-cloud/storage');
const sinon = require('sinon');
const {request} = require('gaxios');
Expand Down Expand Up @@ -62,11 +62,6 @@ async function startFF(port) {
return {ffProc, ffProcHandler};
}

// ImageMagick is available by default in Cloud Run Functions environments
// https://cloud.google.com/functions/1stgendocs/tutorials/imagemagick-1st-gen.md#importing_dependencies
// Manually install it for testing only.
execSync('sudo apt-get install imagemagick -y');

describe('functions/imagemagick tests', () => {
before(async () => {
let exists;
Expand All @@ -92,40 +87,54 @@ describe('functions/imagemagick tests', () => {
it('blurOffensiveImages detects safe images using Cloud Vision', async () => {
const PORT = 8080;
const {ffProc, ffProcHandler} = await startFF(PORT);

await request({
url: `http://localhost:${PORT}/blurOffensiveImages`,
method: 'POST',
data: {
let stdout;
try {
await request({
url: `http://localhost:${PORT}/blurOffensiveImages`,
method: 'POST',
data: {
bucket: BUCKET_NAME,
name: testFiles.safe,
data: {
bucket: BUCKET_NAME,
name: testFiles.safe,
},
},
},
});
ffProc.kill();
const stdout = await ffProcHandler;
});
} catch (err) {
console.error(
`Cloud Function Error: ${err.response?.data || err.message}`
);
throw err;
} finally {
ffProc.kill();
stdout = await ffProcHandler;
}
assert.ok(stdout.includes(`Detected ${testFiles.safe} as OK.`));
});

it('blurOffensiveImages successfully blurs offensive images', async () => {
const PORT = 8081;
const {ffProc, ffProcHandler} = await startFF(PORT);

await request({
url: `http://localhost:${PORT}/blurOffensiveImages`,
method: 'POST',
data: {
let stdout;
try {
await request({
url: `http://localhost:${PORT}/blurOffensiveImages`,
method: 'POST',
data: {
bucket: BUCKET_NAME,
name: testFiles.offensive,
data: {
bucket: BUCKET_NAME,
name: testFiles.offensive,
},
},
},
});

ffProc.kill();
const stdout = await ffProcHandler;

});
} catch (err) {
console.error(
`Cloud Function Error: ${err.response?.data || err.message}`
);
throw err;
} finally {
ffProc.kill();
stdout = await ffProcHandler;
}
assert.ok(stdout.includes(`Blurred image: ${testFiles.offensive}`));
assert.ok(
stdout.includes(
Expand Down
Loading