From 5c72f51c76b6a3e77b9513ebfd57fbb124855ea0 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Mon, 20 Apr 2026 22:39:09 +0000 Subject: [PATCH 1/6] refactor: replace deprecated gm library with sharp in v2 sample --- functions/v2/imagemagick/index.js | 35 +++++++++---------- functions/v2/imagemagick/package.json | 4 +-- .../v2/imagemagick/test/integration.test.js | 6 ---- functions/v2/imagemagick/test/unit.test.js | 18 +++++----- 4 files changed, 27 insertions(+), 36 deletions(-) diff --git a/functions/v2/imagemagick/index.js b/functions/v2/imagemagick/index.js index e6f32f0837..2c6adcda2a 100644 --- a/functions/v2/imagemagick/index.js +++ b/functions/v2/imagemagick/index.js @@ -16,7 +16,7 @@ // [START functions_imagemagick_setup] const functions = require('@google-cloud/functions-framework'); -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'); @@ -64,6 +64,7 @@ functions.cloudEvent('blurOffensiveImages', async cloudEvent => { // Blurs the given file using ImageMagick, 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 { @@ -74,19 +75,14 @@ const blurImage = async (file, blurredBucketName) => { throw new Error(`File download failed: ${err}`); } - 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); - } - }); - }); + try { + await sharp(tempLocalPath).blur(16).toFile(tempLocalBlurredPath); + + 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); @@ -94,13 +90,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/v2/imagemagick/package.json b/functions/v2/imagemagick/package.json index 43e1ac3d46..774d219c1a 100644 --- a/functions/v2/imagemagick/package.json +++ b/functions/v2/imagemagick/package.json @@ -18,7 +18,7 @@ "@google-cloud/functions-framework": "^3.1.0", "@google-cloud/storage": "^7.0.0", "@google-cloud/vision": "^4.0.0", - "gm": "^1.23.1" + "sharp": "^0.34.5" }, "devDependencies": { "c8": "^10.0.0", @@ -27,4 +27,4 @@ "sinon": "^18.0.0", "supertest": "^7.0.0" } -} +} \ No newline at end of file diff --git a/functions/v2/imagemagick/test/integration.test.js b/functions/v2/imagemagick/test/integration.test.js index 82f5b8a43e..e0a2ac56d2 100644 --- a/functions/v2/imagemagick/test/integration.test.js +++ b/functions/v2/imagemagick/test/integration.test.js @@ -15,7 +15,6 @@ 'use strict'; const assert = require('assert'); -const {execSync} = require('child_process'); const {Storage} = require('@google-cloud/storage'); const sinon = require('sinon'); const supertest = require('supertest'); @@ -34,11 +33,6 @@ const testFiles = { require('../index'); -// 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; diff --git a/functions/v2/imagemagick/test/unit.test.js b/functions/v2/imagemagick/test/unit.test.js index acf453198d..f502441a01 100644 --- a/functions/v2/imagemagick/test/unit.test.js +++ b/functions/v2/imagemagick/test/unit.test.js @@ -38,31 +38,29 @@ const loadSample = (adultResult, fileName) => { return { bucket: sinon.stub().returnsThis(), file: sinon.stub().returnsThis(), - upload: sinon.stub().returnsThis(), - download: sinon.stub().returnsThis(), + upload: sinon.stub().resolves(), + download: sinon.stub().resolves(), name: fileName, }; }, }; - const gm = () => { - return { - blur: sinon.stub().returnsThis(), - write: sinon.stub().yields(), - }; + const sharpInstance = { + blur: sinon.stub().returnsThis(), + toFile: sinon.stub().resolves(), }; - gm.subClass = sinon.stub().returnsThis(); + const sharpMock = sinon.stub().returns(sharpInstance); const fs = { promises: { - unlink: sinon.stub(), + unlink: sinon.stub().resolves(), }, }; return proxyquire('..', { '@google-cloud/vision': vision, '@google-cloud/storage': storage, - gm: gm, + sharp: sharpMock, fs: fs, }); }; From 128cfe30c99b2eed600559c2303d8a65d1560a57 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Mon, 20 Apr 2026 22:53:56 +0000 Subject: [PATCH 2/6] chore: update node version in package.json --- functions/v2/imagemagick/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/v2/imagemagick/package.json b/functions/v2/imagemagick/package.json index 774d219c1a..3b9b2ede57 100644 --- a/functions/v2/imagemagick/package.json +++ b/functions/v2/imagemagick/package.json @@ -9,7 +9,7 @@ "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.17.0" }, "scripts": { "test": "c8 mocha -p -j 2 test/*.test.js --timeout=20000 --exit" From 31c9973c5bcf3a696fcc0e2a191c6b5618183b90 Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Mon, 20 Apr 2026 20:34:00 -0700 Subject: [PATCH 3/6] fix: handle condition where bucket is the same as blurred bucket --- functions/v2/imagemagick/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/functions/v2/imagemagick/index.js b/functions/v2/imagemagick/index.js index 2c6adcda2a..5628af2c11 100644 --- a/functions/v2/imagemagick/index.js +++ b/functions/v2/imagemagick/index.js @@ -34,6 +34,12 @@ functions.cloudEvent('blurOffensiveImages', async cloudEvent => { // This event represents the triggering Cloud Storage object. const bucket = cloudEvent.data.bucket; const name = cloudEvent.data.name; + + if (bucket === BLURRED_BUCKET_NAME) { + console.log('Event triggered by the blurred bucket; skip to avoid recursion') + return; + } + const file = storage.bucket(bucket).file(name); const filePath = `gs://${bucket}/${name}`; From 87e64188a72de518d42daf1be076d40a510b0775 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Tue, 21 Apr 2026 16:34:20 +0000 Subject: [PATCH 4/6] fix: remove ImageMagick comments and resolve linting issues --- functions/v2/imagemagick/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/functions/v2/imagemagick/index.js b/functions/v2/imagemagick/index.js index 5628af2c11..a0326ba311 100644 --- a/functions/v2/imagemagick/index.js +++ b/functions/v2/imagemagick/index.js @@ -36,10 +36,12 @@ functions.cloudEvent('blurOffensiveImages', async cloudEvent => { const name = cloudEvent.data.name; if (bucket === BLURRED_BUCKET_NAME) { - console.log('Event triggered by the blurred bucket; skip to avoid recursion') + console.log( + 'Event triggered by the blurred bucket; skip to avoid recursion' + ); return; } - + const file = storage.bucket(bucket).file(name); const filePath = `gs://${bucket}/${name}`; @@ -67,7 +69,7 @@ functions.cloudEvent('blurOffensiveImages', async cloudEvent => { // [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}`; From 848c8fe5901f6038925a61f0a21058425a26cc9f Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Tue, 21 Apr 2026 19:37:17 +0000 Subject: [PATCH 5/6] docs: remove gm library references from README --- functions/v2/imagemagick/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/v2/imagemagick/README.md b/functions/v2/imagemagick/README.md index 6f2962f7ff..78f5a3bd8c 100644 --- a/functions/v2/imagemagick/README.md +++ b/functions/v2/imagemagick/README.md @@ -2,7 +2,7 @@ # 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]. From 4c46db8e22c78e095940affd583d4c67ddab65bd Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Tue, 21 Apr 2026 19:46:54 +0000 Subject: [PATCH 6/6] docs: remove gm library references from README --- functions/v2/imagemagick/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/v2/imagemagick/README.md b/functions/v2/imagemagick/README.md index 78f5a3bd8c..1c7fa3ff4d 100644 --- a/functions/v2/imagemagick/README.md +++ b/functions/v2/imagemagick/README.md @@ -1,6 +1,6 @@ Google Cloud Platform logo -# Google Cloud Functions ImageMagick sample +# Google Cloud Functions imagemagick sample This sample shows you how to blur an image using sharp in a Storage-triggered Cloud Function.