diff --git a/functions/v2/imagemagick/README.md b/functions/v2/imagemagick/README.md index 6f2962f7ff..1c7fa3ff4d 100644 --- a/functions/v2/imagemagick/README.md +++ b/functions/v2/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/v2/imagemagick/index.js b/functions/v2/imagemagick/index.js index e6f32f0837..a0326ba311 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'); @@ -34,6 +34,14 @@ 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}`; @@ -61,9 +69,10 @@ 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}`; // Download file from bucket. try { @@ -74,19 +83,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 +98,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..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" @@ -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, }); };