Skip to content

Commit 90048b1

Browse files
author
Ace Nassri
authored
feat(functions): add ImageMagick samples (GoogleCloudPlatform#1245)
1 parent ed5b64d commit 90048b1

11 files changed

+526
-0
lines changed

.kokoro/secrets-example.sh

+3
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,6 @@ export SYMFONY_CLOUDSQL_CONNECTION_NAME=$CLOUDSQL_CONNECTION_NAME_MYSQL
137137
export SYMFONY_DB_DATABASE=symfony
138138
export SYMFONY_DB_USERNAME=$CLOUDSQL_USER
139139
export SYMFONY_DB_PASSWORD=$CLOUDSQL_PASSWORD
140+
141+
# Functions
142+
export BLURRED_BUCKET_NAME=$GCLOUD_PROJECT-functions

.kokoro/secrets.sh.enc

39 Bytes
Binary file not shown.

functions/imagemagick/.gcloudignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
test/
2+
vendor/
3+
build/

functions/imagemagick/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<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"/>
2+
3+
# Google Cloud Functions ImageMagick sample
4+
5+
This sample shows you how to blur an image using ImageMagick in a
6+
Storage-triggered Cloud Function.
7+
8+
- View the [source code][code].
9+
- See the [tutorial].
10+
11+
**Note:** This example requires the `imagick` PECL package.
12+
13+
[code]: index.php
14+
[tutorial]: https://cloud.google.com/functions/docs/tutorials/imagemagick

functions/imagemagick/composer.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"require": {
3+
"google/cloud-functions-framework": "^0.7",
4+
"google/cloud-storage": "^1.23",
5+
"google/cloud-vision": "^1.2",
6+
"ext-imagick": "*"
7+
},
8+
"require-dev": {
9+
"google/cloud-logging": "^1.21"
10+
}
11+
}

functions/imagemagick/index.php

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
// [START functions_imagemagick_setup]
19+
use Google\CloudFunctions\CloudEvent;
20+
use Google\Cloud\Storage\StorageClient;
21+
use Google\Cloud\Vision\V1\ImageAnnotatorClient;
22+
use Google\Cloud\Vision\V1\Likelihood;
23+
24+
// [END functions_imagemagick_setup]
25+
26+
// [START functions_imagemagick_analyze]
27+
function blurOffensiveImages(CloudEvent $cloudevent): void
28+
{
29+
$log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb');
30+
$storage = new StorageClient();
31+
$data = $cloudevent->getData();
32+
33+
$file = $storage->bucket($data['bucket'])->object($data['name']);
34+
$filePath = 'gs://' . $data['bucket'] . '/' . $data['name'];
35+
fwrite($log, 'Analyzing ' . $filePath . PHP_EOL);
36+
37+
$annotator = new ImageAnnotatorClient();
38+
$storage = new StorageClient();
39+
40+
try {
41+
$request = $annotator->safeSearchDetection($filePath);
42+
$response = $request->getSafeSearchAnnotation();
43+
44+
// Handle missing files
45+
// (This is uncommon, but can happen if race conditions occur)
46+
if ($response === null) {
47+
fwrite($log, 'Could not find ' . $filePath . PHP_EOL);
48+
return;
49+
}
50+
51+
$isInappropriate =
52+
$response->getAdult() === Likelihood::VERY_LIKELY ||
53+
$response->getViolence() === Likelihood::VERY_LIKELY;
54+
55+
if ($isInappropriate) {
56+
fwrite($log, 'Detected ' . $data['name'] . ' as inappropriate.' . PHP_EOL);
57+
$blurredBucketName = getenv('BLURRED_BUCKET_NAME');
58+
59+
blurImage($log, $file, $blurredBucketName);
60+
} else {
61+
fwrite($log, 'Detected ' . $data['name'] . ' as OK.' . PHP_EOL);
62+
}
63+
} catch (Exception $e) {
64+
fwrite($log, 'Failed to analyze ' . $data['name'] . PHP_EOL);
65+
fwrite($log, $e->getMessage() . PHP_EOL);
66+
}
67+
}
68+
// [END functions_imagemagick_analyze]
69+
70+
// [START functions_imagemagick_blur]
71+
// Blurs the given file using ImageMagick, and uploads it to another bucket.
72+
function blurImage(
73+
$log,
74+
Object $file,
75+
string $blurredBucketName
76+
): void {
77+
$tempLocalPath = sys_get_temp_dir() . '/' . $file->name();
78+
79+
// Download file from bucket.
80+
$image = new Imagick();
81+
try {
82+
$image->readImageBlob($file->downloadAsStream());
83+
} catch (Exception $e) {
84+
throw new Exception('Streaming download failed: ' . $e);
85+
}
86+
87+
// Blur file using ImageMagick
88+
// (The Imagick class is from the PECL 'imagick' package)
89+
$image->blurImage(0, 16);
90+
91+
// Stream blurred image result to a different bucket. // (This avoids re-triggering this function.)
92+
$storage = new StorageClient();
93+
$blurredBucket = $storage->bucket($blurredBucketName);
94+
95+
// Upload the Blurred image back into the bucket.
96+
$gcsPath = 'gs://' . $blurredBucketName . '/' . $file->name();
97+
try {
98+
$blurredBucket->upload($image->getImageBlob(), [
99+
'name' => $file->name()
100+
]);
101+
fwrite($log, 'Streamed blurred image to: ' . $gcsPath . PHP_EOL);
102+
} catch (Exception $e) {
103+
throw new Exception(
104+
sprintf('Unable to stream blurred image to %s: %s',
105+
$gcsPath,
106+
$e->getMessage()
107+
)
108+
);
109+
}
110+
}
111+
// [END functions_imagemagick_blur]

functions/imagemagick/php.ini

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
; [START functions_imagemagick_php_ini]
2+
; The imagick PHP extension is installed but disabled by default.
3+
; See this page for a list of available extensions:
4+
; https://cloud.google.com/functions/docs/concepts/php-runtime
5+
6+
extension=imagick.so
7+
; [END functions_imagemagick_php_ini]
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Copyright 2020 Google LLC
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<phpunit bootstrap="../../testing/bootstrap.php" convertWarningsToExceptions="false">
18+
<testsuites>
19+
<testsuite name="Cloud Functions ImageMagick Test Suite">
20+
<directory>test</directory>
21+
</testsuite>
22+
</testsuites>
23+
<logging>
24+
<log type="coverage-clover" target="build/logs/clover.xml"/>
25+
</logging>
26+
<filter>
27+
<whitelist>
28+
<directory suffix=".php">.</directory>
29+
<exclude>
30+
<directory>./vendor</directory>
31+
</exclude>
32+
</whitelist>
33+
</filter>
34+
</phpunit>
+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\ImageMagick\Test;
21+
22+
use Google\Cloud\Storage\StorageClient;
23+
use Google\Cloud\Logging\LoggingClient;
24+
use Google\Cloud\TestUtils\CloudFunctionDeploymentTrait;
25+
use Google\Cloud\TestUtils\EventuallyConsistentTestTrait;
26+
use Google\Cloud\TestUtils\GcloudWrapper\CloudFunction;
27+
use PHPUnit\Framework\ExpectationFailedException;
28+
use PHPUnit\Framework\TestCase;
29+
30+
require_once __DIR__ . '/TestCasesTrait.php';
31+
32+
/**
33+
* Class DeployTest.
34+
*
35+
* This test is not run by the CI system.
36+
*
37+
* To skip deployment of a new function, run with "GOOGLE_SKIP_DEPLOYMENT=true".
38+
* To skip deletion of the tested function, run with "GOOGLE_KEEP_DEPLOYMENT=true".
39+
*/
40+
class DeployTest extends TestCase
41+
{
42+
use CloudFunctionDeploymentTrait;
43+
use EventuallyConsistentTestTrait;
44+
use TestCasesTrait;
45+
46+
/** @var string */
47+
private static $entryPoint = 'blurOffensiveImages';
48+
49+
/** @var string */
50+
private static $functionSignatureType = 'cloudevent';
51+
52+
/** @var string */
53+
// The test starts by copying images from this bucket.
54+
private const FIXTURE_SOURCE_BUCKET = 'cloud-devrel-public';
55+
56+
/** @var string */
57+
// This is the bucket the deployed function monitors.
58+
// The test copies image from FIXTURE_SOURCE_BUCKET to this one.
59+
private static $monitoredBucket;
60+
61+
/** @var string */
62+
// The function saves any blurred images to this bucket.
63+
private static $blurredBucket;
64+
65+
/** @var StorageClient */
66+
private static $storageClient;
67+
68+
/** @var LoggingClient */
69+
private static $loggingClient;
70+
71+
/**
72+
* @dataProvider cases
73+
*/
74+
public function testFunction(
75+
$cloudevent,
76+
$label,
77+
$fileName,
78+
$expected,
79+
$statusCode
80+
): void {
81+
// Upload target file.
82+
$fixtureBucket = self::$storageClient->bucket(self::FIXTURE_SOURCE_BUCKET);
83+
$object = $fixtureBucket->object($fileName);
84+
85+
$object->copy(self::$monitoredBucket, ['name' => $fileName]);
86+
87+
// Give event and log systems a head start.
88+
// If log retrieval fails to find logs for our function within retry limit, increase sleep time.
89+
sleep(5);
90+
91+
$fiveMinAgo = date(\DateTime::RFC3339, strtotime('-5 minutes'));
92+
$this->processFunctionLogs(self::$fn, $fiveMinAgo, function (\Iterator $logs) use ($expected, $label) {
93+
// Concatenate all relevant log messages.
94+
$actual = '';
95+
foreach ($logs as $log) {
96+
$info = $log->info();
97+
$actual .= $info['textPayload'];
98+
}
99+
100+
// Only testing one property to decrease odds the expected logs are
101+
// split between log requests.
102+
$this->assertStringContainsString($expected, $actual, $label . ':');
103+
});
104+
}
105+
106+
/**
107+
* Retrieve and process logs for the defined function.
108+
*
109+
* @param CloudFunction $fn function whose logs should be checked.
110+
* @param string $startTime RFC3339 timestamp marking start of time range to retrieve.
111+
* @param callable $process callback function to run on the logs.
112+
*/
113+
private function processFunctionLogs(CloudFunction $fn, string $startTime, callable $process)
114+
{
115+
$projectId = self::requireEnv('GOOGLE_PROJECT_ID');
116+
117+
if (empty(self::$loggingClient)) {
118+
self::$loggingClient = new LoggingClient([
119+
'projectId' => $projectId
120+
]);
121+
}
122+
123+
// Define the log search criteria.
124+
$logFullName = 'projects/' . $projectId . '/logs/cloudfunctions.googleapis.com%2Fcloud-functions';
125+
$filter = sprintf(
126+
'logName="%s" resource.labels.function_name="%s" timestamp>="%s"',
127+
$logFullName,
128+
$fn->getFunctionName(),
129+
$startTime
130+
);
131+
132+
echo "\nRetrieving logs [$filter]...\n";
133+
134+
// Check for new logs for the function.
135+
$attempt = 1;
136+
$this->runEventuallyConsistentTest(function () use ($filter, $process, &$attempt) {
137+
$entries = self::$loggingClient->entries(['filter' => $filter]);
138+
139+
// If no logs came in try again.
140+
if (empty($entries->current())) {
141+
echo 'Logs not found, attempting retry #' . $attempt++ . PHP_EOL;
142+
throw new ExpectationFailedException('Log Entries not available');
143+
}
144+
echo 'Processing logs...' . PHP_EOL;
145+
146+
$process($entries);
147+
}, $retries = 10);
148+
}
149+
150+
/**
151+
* Deploy the Function.
152+
*
153+
* Overrides CloudFunctionLocalTestTrait::doDeploy().
154+
*/
155+
private static function doDeploy()
156+
{
157+
// Initialize variables
158+
if (empty(self::$monitoredBucket)) {
159+
self::$monitoredBucket = self::requireEnv('GOOGLE_STORAGE_BUCKET');
160+
}
161+
if (empty(self::$blurredBucket)) {
162+
self::$blurredBucket = self::requireEnv('BLURRED_BUCKET_NAME');
163+
}
164+
165+
if (empty(self::$storageClient)) {
166+
self::$storageClient = new StorageClient();
167+
}
168+
169+
// Forward required env variables to Cloud Functions.
170+
$envVars = 'GOOGLE_STORAGE_BUCKET=' . self::$monitoredBucket . ',';
171+
$envVars .= 'BLURRED_BUCKET_NAME=' . self::$blurredBucket;
172+
173+
self::$fn->deploy(
174+
['--update-env-vars' => $envVars],
175+
'--trigger-bucket=' . self::$monitoredBucket
176+
);
177+
}
178+
}

0 commit comments

Comments
 (0)