Skip to content

Commit 6a530b5

Browse files
angelcaamaliennae
andauthored
fix(functions): migrate imagemagick sample to sharp and improve test stability (#4288)
* 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. * fix: address code review comments on imagemagick sample * remove all comments related to the gm library * chore: update node version in package.json * fix: remove ImageMagick comments and resolve linting issues * docs: remove gm library references from README --------- Co-authored-by: Jennifer Davis <sigje@google.com>
1 parent c43918f commit 6a530b5

4 files changed

Lines changed: 66 additions & 53 deletions

File tree

functions/imagemagick/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<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"/>
22

3-
# Google Cloud Functions ImageMagick sample
3+
# Google Cloud Functions imagemagick sample
44

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

88
View the [source code][code].

functions/imagemagick/index.js

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
'use strict';
1616

1717
// [START functions_imagemagick_setup]
18-
const gm = require('gm').subClass({imageMagick: true});
18+
const sharp = require('sharp');
1919
const fs = require('fs').promises;
2020
const path = require('path');
2121
const vision = require('@google-cloud/vision');
@@ -32,6 +32,12 @@ const {BLURRED_BUCKET_NAME} = process.env;
3232
exports.blurOffensiveImages = async event => {
3333
// This event represents the triggering Cloud Storage object.
3434
const object = event;
35+
if (object.bucket === BLURRED_BUCKET_NAME) {
36+
console.log(
37+
'Event triggered by the blurred bucket; skip to avoid recursion'
38+
);
39+
return;
40+
}
3541

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

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

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

76-
await new Promise((resolve, reject) => {
77-
gm(tempLocalPath)
78-
.blur(0, 16)
79-
.write(tempLocalPath, (err, stdout) => {
80-
if (err) {
81-
console.error('Failed to blur image.', err);
82-
reject(err);
83-
} else {
84-
console.log(`Blurred image: ${file.name}`);
85-
resolve(stdout);
86-
}
87-
});
88-
});
85+
console.log(`Blurred image: ${file.name}`);
86+
} catch (err) {
87+
console.error('Failed to blur image.', err);
88+
throw err;
89+
}
8990

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

9394
// Upload the Blurred image back into the bucket.
9495
const gcsPath = `gs://${blurredBucketName}/${file.name}`;
9596
try {
96-
await blurredBucket.upload(tempLocalPath, {destination: file.name});
97+
await blurredBucket.upload(tempLocalBlurredPath, {destination: file.name});
9798
console.log(`Uploaded blurred image to: ${gcsPath}`);
9899
} catch (err) {
99100
throw new Error(`Unable to upload blurred image to ${gcsPath}: ${err}`);
101+
} finally {
102+
// Delete the temporary file.
103+
await Promise.allSettled([
104+
fs.unlink(tempLocalPath),
105+
fs.unlink(tempLocalBlurredPath),
106+
]);
100107
}
101-
102-
// Delete the temporary file.
103-
return fs.unlink(tempLocalPath);
104108
};
105109
// [END functions_imagemagick_blur]

functions/imagemagick/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99
"url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
1010
},
1111
"engines": {
12-
"node": ">=12.0.0"
12+
"node": ">=18.17.0"
1313
},
1414
"scripts": {
1515
"test": "c8 mocha -p -j 2 test/*.test.js --timeout=30000 --exit"
1616
},
1717
"dependencies": {
1818
"@google-cloud/storage": "^7.0.0",
1919
"@google-cloud/vision": "^4.0.0",
20-
"gm": "^1.23.1"
20+
"sharp": "^0.34.5"
2121
},
2222
"devDependencies": {
2323
"@google-cloud/functions-framework": "^3.0.0",

functions/imagemagick/test/index.test.js

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
'use strict';
1616

1717
const assert = require('assert');
18-
const {execSync, spawn} = require('child_process');
18+
const {spawn} = require('child_process');
1919
const {Storage} = require('@google-cloud/storage');
2020
const sinon = require('sinon');
2121
const {request} = require('gaxios');
@@ -62,11 +62,6 @@ async function startFF(port) {
6262
return {ffProc, ffProcHandler};
6363
}
6464

65-
// ImageMagick is available by default in Cloud Run Functions environments
66-
// https://cloud.google.com/functions/1stgendocs/tutorials/imagemagick-1st-gen.md#importing_dependencies
67-
// Manually install it for testing only.
68-
execSync('sudo apt-get install imagemagick -y');
69-
7065
describe('functions/imagemagick tests', () => {
7166
before(async () => {
7267
let exists;
@@ -92,40 +87,54 @@ describe('functions/imagemagick tests', () => {
9287
it('blurOffensiveImages detects safe images using Cloud Vision', async () => {
9388
const PORT = 8080;
9489
const {ffProc, ffProcHandler} = await startFF(PORT);
95-
96-
await request({
97-
url: `http://localhost:${PORT}/blurOffensiveImages`,
98-
method: 'POST',
99-
data: {
90+
let stdout;
91+
try {
92+
await request({
93+
url: `http://localhost:${PORT}/blurOffensiveImages`,
94+
method: 'POST',
10095
data: {
101-
bucket: BUCKET_NAME,
102-
name: testFiles.safe,
96+
data: {
97+
bucket: BUCKET_NAME,
98+
name: testFiles.safe,
99+
},
103100
},
104-
},
105-
});
106-
ffProc.kill();
107-
const stdout = await ffProcHandler;
101+
});
102+
} catch (err) {
103+
console.error(
104+
`Cloud Function Error: ${err.response?.data || err.message}`
105+
);
106+
throw err;
107+
} finally {
108+
ffProc.kill();
109+
stdout = await ffProcHandler;
110+
}
108111
assert.ok(stdout.includes(`Detected ${testFiles.safe} as OK.`));
109112
});
110113

111114
it('blurOffensiveImages successfully blurs offensive images', async () => {
112115
const PORT = 8081;
113116
const {ffProc, ffProcHandler} = await startFF(PORT);
114-
115-
await request({
116-
url: `http://localhost:${PORT}/blurOffensiveImages`,
117-
method: 'POST',
118-
data: {
117+
let stdout;
118+
try {
119+
await request({
120+
url: `http://localhost:${PORT}/blurOffensiveImages`,
121+
method: 'POST',
119122
data: {
120-
bucket: BUCKET_NAME,
121-
name: testFiles.offensive,
123+
data: {
124+
bucket: BUCKET_NAME,
125+
name: testFiles.offensive,
126+
},
122127
},
123-
},
124-
});
125-
126-
ffProc.kill();
127-
const stdout = await ffProcHandler;
128-
128+
});
129+
} catch (err) {
130+
console.error(
131+
`Cloud Function Error: ${err.response?.data || err.message}`
132+
);
133+
throw err;
134+
} finally {
135+
ffProc.kill();
136+
stdout = await ffProcHandler;
137+
}
129138
assert.ok(stdout.includes(`Blurred image: ${testFiles.offensive}`));
130139
assert.ok(
131140
stdout.includes(

0 commit comments

Comments
 (0)