diff --git a/functions/imagemagick/README.md b/functions/imagemagick/README.md index 6f2962f7ff..1c7fa3ff4d 100644 --- a/functions/imagemagick/README.md +++ b/functions/imagemagick/README.md @@ -1,8 +1,8 @@ Google Cloud Platform logo -# 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]. diff --git a/functions/imagemagick/index.js b/functions/imagemagick/index.js index ec231bd17c..cf782d2a6f 100644 --- a/functions/imagemagick/index.js +++ b/functions/imagemagick/index.js @@ -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'); @@ -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}`; @@ -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 { @@ -72,20 +79,14 @@ 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); @@ -93,13 +94,16 @@ const blurImage = async (file, 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] diff --git a/functions/imagemagick/package.json b/functions/imagemagick/package.json index cf6fa1cc8b..5b7a94b5d6 100644 --- a/functions/imagemagick/package.json +++ b/functions/imagemagick/package.json @@ -9,7 +9,7 @@ "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" @@ -17,7 +17,7 @@ "dependencies": { "@google-cloud/storage": "^7.0.0", "@google-cloud/vision": "^4.0.0", - "gm": "^1.23.1" + "sharp": "^0.34.5" }, "devDependencies": { "@google-cloud/functions-framework": "^3.0.0", diff --git a/functions/imagemagick/test/index.test.js b/functions/imagemagick/test/index.test.js index 55fecb924c..a6894fe651 100644 --- a/functions/imagemagick/test/index.test.js +++ b/functions/imagemagick/test/index.test.js @@ -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'); @@ -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; @@ -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(