From e706d49e9c6116b7ba9de65e0d354deba90ad492 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Thu, 16 Apr 2026 21:22:39 +0000 Subject: [PATCH 1/6] fix(functions): migrate imagemagick sample to sharp and improve test stability - Replaced 'gm' with 'sharp' for image blurring to fix EPIPE errors. - Fixed temporary file deletion to prevent memory leaks in the blurring function. - Refactored the test suite to use try...finally, ensuring the Functions Framework process is always killed to release ports (fixes EADDRINUSE). - Improved test error logging for easier debugging. --- functions/imagemagick/index.js | 30 +++++------ functions/imagemagick/package.json | 2 +- functions/imagemagick/test/index.test.js | 66 ++++++++++++++---------- 3 files changed, 55 insertions(+), 43 deletions(-) diff --git a/functions/imagemagick/index.js b/functions/imagemagick/index.js index ec231bd17c..dddb32266a 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'); @@ -63,6 +63,7 @@ exports.blurOffensiveImages = async event => { // 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}`; // 1. Declarar la variable // Download file from bucket. try { @@ -72,20 +73,16 @@ const blurImage = async (file, blurredBucketName) => { } catch (err) { throw new Error(`File download failed: ${err}`); } + try { + await sharp(tempLocalPath) + .blur(16) // A sigma of 16 provides a strong blur equivalent to the old gm settings + .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 +90,14 @@ 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}`); } // Delete the temporary file. - return fs.unlink(tempLocalPath); + await fs.unlink(tempLocalPath); + return fs.unlink(tempLocalBlurredPath); }; // [END functions_imagemagick_blur] diff --git a/functions/imagemagick/package.json b/functions/imagemagick/package.json index cf6fa1cc8b..78ef9dd8ac 100644 --- a/functions/imagemagick/package.json +++ b/functions/imagemagick/package.json @@ -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..9541c350a8 100644 --- a/functions/imagemagick/test/index.test.js +++ b/functions/imagemagick/test/index.test.js @@ -65,7 +65,7 @@ async function startFF(port) { // 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'); +//execSync('sudo apt-get install imagemagick -y'); describe('functions/imagemagick tests', () => { before(async () => { @@ -92,40 +92,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( @@ -164,7 +178,7 @@ describe('functions/imagemagick tests', () => { after(async () => { try { - await blurredBucket.file(testFiles.offensive).delete(); + // await blurredBucket.file(testFiles.offensive).delete(); } catch (err) { console.log('Error deleting uploaded file:', err.message); } From 766d265fb19c652438145374233120e705c9ec9d Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Thu, 16 Apr 2026 21:39:42 +0000 Subject: [PATCH 2/6] fix: address code review comments on imagemagick sample --- functions/imagemagick/index.js | 4 ++-- functions/imagemagick/test/index.test.js | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/functions/imagemagick/index.js b/functions/imagemagick/index.js index dddb32266a..a610d2d996 100644 --- a/functions/imagemagick/index.js +++ b/functions/imagemagick/index.js @@ -63,7 +63,7 @@ exports.blurOffensiveImages = async event => { // 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}`; // 1. Declarar la variable + const tempLocalBlurredPath = `/tmp/blurred-${path.parse(file.name).base}`; // Download file from bucket. try { @@ -98,6 +98,6 @@ const blurImage = async (file, blurredBucketName) => { // Delete the temporary file. await fs.unlink(tempLocalPath); - return fs.unlink(tempLocalBlurredPath); + return await fs.unlink(tempLocalBlurredPath); }; // [END functions_imagemagick_blur] diff --git a/functions/imagemagick/test/index.test.js b/functions/imagemagick/test/index.test.js index 9541c350a8..028c858a49 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; @@ -178,7 +173,7 @@ describe('functions/imagemagick tests', () => { after(async () => { try { - // await blurredBucket.file(testFiles.offensive).delete(); + await blurredBucket.file(testFiles.offensive).delete(); } catch (err) { console.log('Error deleting uploaded file:', err.message); } From 5d0e356b7e46aa00f7980098df03522b572a2f36 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Fri, 17 Apr 2026 20:33:49 +0000 Subject: [PATCH 3/6] remove all comments related to the gm library --- functions/imagemagick/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/functions/imagemagick/index.js b/functions/imagemagick/index.js index a610d2d996..8fd6d51453 100644 --- a/functions/imagemagick/index.js +++ b/functions/imagemagick/index.js @@ -74,9 +74,7 @@ const blurImage = async (file, blurredBucketName) => { throw new Error(`File download failed: ${err}`); } try { - await sharp(tempLocalPath) - .blur(16) // A sigma of 16 provides a strong blur equivalent to the old gm settings - .toFile(tempLocalBlurredPath); + await sharp(tempLocalPath).blur(16).toFile(tempLocalBlurredPath); console.log(`Blurred image: ${file.name}`); } catch (err) { From 1699ae45ca862d7f1683389f4e14cafca139d6c9 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Mon, 20 Apr 2026 23:00:35 +0000 Subject: [PATCH 4/6] chore: update node version in package.json --- functions/imagemagick/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/imagemagick/package.json b/functions/imagemagick/package.json index 78ef9dd8ac..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" From 307e8b9d190554a6eb685f810631ff79692b2645 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Tue, 21 Apr 2026 16:53:48 +0000 Subject: [PATCH 5/6] fix: remove ImageMagick comments and resolve linting issues --- functions/imagemagick/index.js | 18 +++++++++++++----- functions/imagemagick/test/index.test.js | 4 ++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/functions/imagemagick/index.js b/functions/imagemagick/index.js index 8fd6d51453..cf782d2a6f 100644 --- a/functions/imagemagick/index.js +++ b/functions/imagemagick/index.js @@ -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,7 +66,7 @@ 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}`; @@ -92,10 +98,12 @@ const blurImage = async (file, blurredBucketName) => { 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. - await fs.unlink(tempLocalPath); - return await fs.unlink(tempLocalBlurredPath); }; // [END functions_imagemagick_blur] diff --git a/functions/imagemagick/test/index.test.js b/functions/imagemagick/test/index.test.js index 028c858a49..a6894fe651 100644 --- a/functions/imagemagick/test/index.test.js +++ b/functions/imagemagick/test/index.test.js @@ -101,7 +101,7 @@ describe('functions/imagemagick tests', () => { }); } catch (err) { console.error( - `[Cloud Function Error]: ${err.response?.data || err.message}` + `Cloud Function Error: ${err.response?.data || err.message}` ); throw err; } finally { @@ -128,7 +128,7 @@ describe('functions/imagemagick tests', () => { }); } catch (err) { console.error( - `[Cloud Function Error]: ${err.response?.data || err.message}` + `Cloud Function Error: ${err.response?.data || err.message}` ); throw err; } finally { From 28ff9e6f67ee37fe616038e2b13b01fd79695778 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Tue, 21 Apr 2026 19:44:26 +0000 Subject: [PATCH 6/6] docs: remove gm library references from README --- functions/imagemagick/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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].