From 16a674b2da878ff3e7dff46db780eccb489a1582 Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Thu, 30 Aug 2018 19:23:03 -0700 Subject: [PATCH 1/8] Initial commit of imagemagick sample Change-Id: Ie8aca88b0dc80926438c9e87b2e71071f8f294d6 --- functions/imagemagick/README.md | 47 ++++++++++++++ functions/imagemagick/main.py | 84 ++++++++++++++++++++++++++ functions/imagemagick/requirements.txt | 2 + 3 files changed, 133 insertions(+) create mode 100644 functions/imagemagick/README.md create mode 100644 functions/imagemagick/main.py create mode 100644 functions/imagemagick/requirements.txt diff --git a/functions/imagemagick/README.md b/functions/imagemagick/README.md new file mode 100644 index 00000000000..549c2f32186 --- /dev/null +++ b/functions/imagemagick/README.md @@ -0,0 +1,47 @@ +Google Cloud Platform logo + +# Google Cloud Functions ImageMagick sample + +This sample shows you how to blur an image using ImageMagick in a +Storage-triggered Cloud Function. + +View the [source code][code]. + +[code]: main.py + +## Deploy and Test + +1. Follow the [Cloud Functions quickstart guide][quickstart] to setup Cloud +Functions for your project. + +1. Clone this repository: + + git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git + cd python-docs-samples/functions/imagemagick + +1. Create a Cloud Storage Bucket: + + gsutil mb gs://YOUR_BUCKET_NAME + + This storage bucket is used to upload images for the function to check. + +1. Deploy the `blur_offensive_images` function with a Storage trigger: + + gcloud functions deploy blur_offensive_images --trigger-bucket=YOUR_BUCKET_NAME --runtime python37 + + * Replace `YOUR_BUCKET_NAME` with the name of the Cloud Storage Bucket you created earlier. + +1. Upload an offensive image to the Storage bucket, such as this image of + a flesh-eating zombie: https://cdn.pixabay.com/photo/2015/09/21/14/24/zombie-949916_1280.jpg + +1. Check the logs for the `blur_offensive_images` function: + + gcloud functions get-logs blur_offensive_images + + You should see something like this in your console: + + D ... User function triggered, starting execution + I ... `The image zombie.jpg has been detected as inappropriate.` + D ... Execution took 1 ms, user function completed successfully + +[quickstart]: https://cloud.google.com/functions/quickstart diff --git a/functions/imagemagick/main.py b/functions/imagemagick/main.py new file mode 100644 index 00000000000..035d2b826b2 --- /dev/null +++ b/functions/imagemagick/main.py @@ -0,0 +1,84 @@ +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# [START functions_imagemagick_setup] +import os +import subprocess + +from google.cloud import storage, vision + + +storage_client = storage.Client() +vision_client = vision.ImageAnnotatorClient() +# [END functions_imagemagick_setup] + + +# [START functions_imagemagick_analyze] +# Blurs uploaded images that are flagged as Adult or Violence. +def blur_offensive_images(data, context): + blob = data + + # Exit if this is a deletion or a deploy event. + if blob.resource_state == 'not_exists': + print('This is a deletion event.') + return + elif 'name' not in blob: + print('This is a deploy event.') + return + + file_name = blob['name'] + file = storage_client.bucket(blob.bucket).file(file_name) + file_path = 'gs://%s/%s' % (blob.bucket, file_name) + + print('Analyzing %s' % file_name) + + result = vision_client.safe_search_detection(file_path) + detected = result.safe_search_annotation + + if detected.adult == 'VERY_LIKELY' or detected.violence == 'VERY_LIKELY': + print('The image %s was detected as inappropriate.' % file_name) + return blur_image(file) + else: + print('The image %s was detected as OK.' % file_name) +# [END functions_imagemagick_analyze] + + +# [START functions_imagemagick_blur] +# Blurs the given file using ImageMagick. +def blur_image(file): + file_name = file["name"] + temp_local_filename = '/tmp/%s' % os.path.basename(file_name) + + # Download file from bucket. + file.download_to_filename(temp_local_filename) + print('Image %s was downloaded to %s.' % (file_name, temp_local_filename)) + + # Blur the image using ImageMagick. + subprocess.check_call([ + 'convert', temp_local_filename, + '-channel', 'RGBA', + '-blur', '0x24', + temp_local_filename + ]) + print('Image %s was blurred.' % file_name) + + # Upload the Blurred image back into the bucket. + blurred_file = file.blob(file_name) + blurred_file.upload_from_file(temp_local_filename) + print('Blurred image was uploaded to %s.' % file_name) + + # Delete the temporary file. + os.remove(temp_local_filename) +# [END functions_imagemagick_blur] diff --git a/functions/imagemagick/requirements.txt b/functions/imagemagick/requirements.txt new file mode 100644 index 00000000000..d2cc1607630 --- /dev/null +++ b/functions/imagemagick/requirements.txt @@ -0,0 +1,2 @@ +google-cloud-vision==0.33.0 +google-cloud-storage==1.11.0 From bba4ff849d9dbd0eac5e978aa4421467949b290e Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Tue, 4 Sep 2018 16:09:02 -0700 Subject: [PATCH 2/8] Add unit tests + get them passing Change-Id: I75ec970fd22c870664e4133f51562793a464df1f --- functions/imagemagick/main.py | 20 ++--- functions/imagemagick/main_test.py | 137 +++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 functions/imagemagick/main_test.py diff --git a/functions/imagemagick/main.py b/functions/imagemagick/main.py index 035d2b826b2..24e3ee99241 100644 --- a/functions/imagemagick/main.py +++ b/functions/imagemagick/main.py @@ -31,25 +31,25 @@ def blur_offensive_images(data, context): blob = data # Exit if this is a deletion or a deploy event. - if blob.resource_state == 'not_exists': - print('This is a deletion event.') - return - elif 'name' not in blob: + if 'name' not in blob: print('This is a deploy event.') return + elif blob['resource_state'] == 'not_exists': + print('This is a deletion event.') + return file_name = blob['name'] - file = storage_client.bucket(blob.bucket).file(file_name) - file_path = 'gs://%s/%s' % (blob.bucket, file_name) + file = storage_client.bucket(blob['bucket']).file(file_name) + file_path = 'gs://%s/%s' % (blob['bucket'], file_name) - print('Analyzing %s' % file_name) + print('Analyzing %s.' % file_name) result = vision_client.safe_search_detection(file_path) detected = result.safe_search_annotation if detected.adult == 'VERY_LIKELY' or detected.violence == 'VERY_LIKELY': print('The image %s was detected as inappropriate.' % file_name) - return blur_image(file) + return __blur_image(file) else: print('The image %s was detected as OK.' % file_name) # [END functions_imagemagick_analyze] @@ -57,8 +57,8 @@ def blur_offensive_images(data, context): # [START functions_imagemagick_blur] # Blurs the given file using ImageMagick. -def blur_image(file): - file_name = file["name"] +def __blur_image(file): + file_name = file['name'] temp_local_filename = '/tmp/%s' % os.path.basename(file_name) # Download file from bucket. diff --git a/functions/imagemagick/main_test.py b/functions/imagemagick/main_test.py new file mode 100644 index 00000000000..8e2d313f3aa --- /dev/null +++ b/functions/imagemagick/main_test.py @@ -0,0 +1,137 @@ +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uuid + +from mock import MagicMock, patch + +import main + + +class DictObject(dict): + pass + + +@patch('main.__blur_image') +def test_do_nothing_on_delete(__blur_image, capsys): + __blur_image = MagicMock() + data = { + 'resource_state': 'not_exists', + 'name': '' + } + + main.blur_offensive_images(data, None) + + out, _ = capsys.readouterr() + assert 'This is a deletion event.' in out + assert __blur_image.called is False + + +@patch('main.__blur_image') +def test_do_nothing_on_deploy(__blur_image, capsys): + __blur_image = MagicMock() + + main.blur_offensive_images({}, None) + + out, _ = capsys.readouterr() + assert 'This is a deploy event.' in out + assert __blur_image.called is False + + +@patch('main.__blur_image') +@patch('main.vision_client') +@patch('main.storage_client') +def test_process_offensive_image( + __blur_image, + vision_client, + storage_client, + capsys): + result = DictObject() + result.safe_search_annotation = DictObject() + result.safe_search_annotation.adult = 'VERY_LIKELY' + result.safe_search_annotation.violence = 'VERY_LIKELY' + vision_client.safe_search_detection = MagicMock(return_value=result) + + filename = str(uuid.uuid4()) + data = { + 'resource_state': '', + 'bucket': 'my-bucket', + 'name': filename + } + + main.blur_offensive_images(data, None) + + out, _ = capsys.readouterr() + assert 'Analyzing %s.' % filename in out + assert 'The image %s was detected as inappropriate.' % filename in out + assert main.__blur_image.called + + +@patch('main.__blur_image') +@patch('main.vision_client') +@patch('main.storage_client') +def test_process_safe_image( + __blur_image, + vision_client, + storage_client, + capsys): + result = DictObject() + result.safe_search_annotation = DictObject() + result.safe_search_annotation.adult = 'VERY_UNLIKELY' + result.safe_search_annotation.violence = 'VERY_UNLIKELY' + vision_client.safe_search_detection = MagicMock(return_value=result) + + filename = str(uuid.uuid4()) + data = { + 'resource_state': '', + 'bucket': 'my-bucket', + 'name': filename + } + + main.blur_offensive_images(data, None) + + out, _ = capsys.readouterr() + assert 'Analyzing %s.' % filename in out + assert 'The image %s was detected as OK.' % filename in out + assert __blur_image.called is False + + +@patch('main.os') +@patch('main.subprocess') +def test_blur_image(subprocess_mock, os_mock, capsys): + filename = str(uuid.uuid4()) + + os_mock.remove = MagicMock() + os_mock.path = MagicMock() + os_mock.path.basename = MagicMock(side_effect=(lambda x: x)) + + subprocess_mock.check_call = MagicMock() + + blob_mock = MagicMock() + blob_mock.upload_from_file = MagicMock() + + file = DictObject() + file['name'] = filename + file.download_to_filename = MagicMock() + file.blob = MagicMock(return_value=blob_mock) + + main.__blur_image(file) + + out, _ = capsys.readouterr() + + assert 'Image %s was downloaded to /tmp/%s.' % (filename, filename) in out + assert 'Image %s was blurred.' % filename in out + assert 'Blurred image was uploaded to %s.' % filename in out + assert os_mock.remove.called + assert subprocess_mock.check_call.called From 17d860f8a68052cbe3b46f23148556cf4225a8a2 Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Tue, 4 Sep 2018 22:06:29 -0700 Subject: [PATCH 3/8] Get sample working on GCF Change-Id: I255301ab7218b591c6c61f7da059a3d577bd3186 --- functions/imagemagick/main.py | 30 ++++++++++++++++-------------- functions/imagemagick/main_test.py | 21 +++++++++------------ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/functions/imagemagick/main.py b/functions/imagemagick/main.py index 24e3ee99241..d0c7041b9b4 100644 --- a/functions/imagemagick/main.py +++ b/functions/imagemagick/main.py @@ -28,28 +28,29 @@ # [START functions_imagemagick_analyze] # Blurs uploaded images that are flagged as Adult or Violence. def blur_offensive_images(data, context): - blob = data + file = data # Exit if this is a deletion or a deploy event. - if 'name' not in blob: + if 'name' not in file: print('This is a deploy event.') return - elif blob['resource_state'] == 'not_exists': + elif file.get('resource_state', None) == 'not_exists': print('This is a deletion event.') return - file_name = blob['name'] - file = storage_client.bucket(blob['bucket']).file(file_name) - file_path = 'gs://%s/%s' % (blob['bucket'], file_name) + file_name = file['name'] + blob = storage_client.bucket(file['bucket']).get_blob(file_name) + blob_uri = 'gs://%s/%s' % (file['bucket'], file_name) + blob_source = {'source': {'image_uri': blob_uri}} print('Analyzing %s.' % file_name) - result = vision_client.safe_search_detection(file_path) + result = vision_client.safe_search_detection(blob_source) detected = result.safe_search_annotation - if detected.adult == 'VERY_LIKELY' or detected.violence == 'VERY_LIKELY': + if detected.adult == 5 or detected.violence == 5: print('The image %s was detected as inappropriate.' % file_name) - return __blur_image(file) + return __blur_image(blob) else: print('The image %s was detected as OK.' % file_name) # [END functions_imagemagick_analyze] @@ -57,12 +58,14 @@ def blur_offensive_images(data, context): # [START functions_imagemagick_blur] # Blurs the given file using ImageMagick. -def __blur_image(file): - file_name = file['name'] +def __blur_image(blob): + print(blob) + + file_name = blob.name temp_local_filename = '/tmp/%s' % os.path.basename(file_name) # Download file from bucket. - file.download_to_filename(temp_local_filename) + blob.download_to_filename(temp_local_filename) print('Image %s was downloaded to %s.' % (file_name, temp_local_filename)) # Blur the image using ImageMagick. @@ -75,8 +78,7 @@ def __blur_image(file): print('Image %s was blurred.' % file_name) # Upload the Blurred image back into the bucket. - blurred_file = file.blob(file_name) - blurred_file.upload_from_file(temp_local_filename) + blob.upload_from_filename(temp_local_filename) print('Blurred image was uploaded to %s.' % file_name) # Delete the temporary file. diff --git a/functions/imagemagick/main_test.py b/functions/imagemagick/main_test.py index 8e2d313f3aa..82d5f731224 100644 --- a/functions/imagemagick/main_test.py +++ b/functions/imagemagick/main_test.py @@ -59,8 +59,8 @@ def test_process_offensive_image( capsys): result = DictObject() result.safe_search_annotation = DictObject() - result.safe_search_annotation.adult = 'VERY_LIKELY' - result.safe_search_annotation.violence = 'VERY_LIKELY' + result.safe_search_annotation.adult = 5 + result.safe_search_annotation.violence = 5 vision_client.safe_search_detection = MagicMock(return_value=result) filename = str(uuid.uuid4()) @@ -88,8 +88,8 @@ def test_process_safe_image( capsys): result = DictObject() result.safe_search_annotation = DictObject() - result.safe_search_annotation.adult = 'VERY_UNLIKELY' - result.safe_search_annotation.violence = 'VERY_UNLIKELY' + result.safe_search_annotation.adult = 1 + result.safe_search_annotation.violence = 1 vision_client.safe_search_detection = MagicMock(return_value=result) filename = str(uuid.uuid4()) @@ -118,15 +118,12 @@ def test_blur_image(subprocess_mock, os_mock, capsys): subprocess_mock.check_call = MagicMock() - blob_mock = MagicMock() - blob_mock.upload_from_file = MagicMock() + blob = DictObject() + blob.name = filename + blob.download_to_filename = MagicMock() + blob.upload_from_filename = MagicMock() - file = DictObject() - file['name'] = filename - file.download_to_filename = MagicMock() - file.blob = MagicMock(return_value=blob_mock) - - main.__blur_image(file) + main.__blur_image(blob) out, _ = capsys.readouterr() From fb919df971d18b8d4cacb948979115df49785b88 Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Wed, 12 Sep 2018 10:20:42 -0700 Subject: [PATCH 4/8] Address comments, pt 1 Change-Id: I31808597e16b7b2201adbd8a51fe84241c90e649 --- functions/imagemagick/main.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/functions/imagemagick/main.py b/functions/imagemagick/main.py index d0c7041b9b4..71fcf603bce 100644 --- a/functions/imagemagick/main.py +++ b/functions/imagemagick/main.py @@ -28,19 +28,11 @@ # [START functions_imagemagick_analyze] # Blurs uploaded images that are flagged as Adult or Violence. def blur_offensive_images(data, context): - file = data - - # Exit if this is a deletion or a deploy event. - if 'name' not in file: - print('This is a deploy event.') - return - elif file.get('resource_state', None) == 'not_exists': - print('This is a deletion event.') - return - - file_name = file['name'] - blob = storage_client.bucket(file['bucket']).get_blob(file_name) - blob_uri = 'gs://%s/%s' % (file['bucket'], file_name) + file_data = data + + file_name = file_data['name'] + blob = storage_client.bucket(file_data['bucket']).get_blob(file_name) + blob_uri = 'gs://%s/%s' % (file_data['bucket'], file_name) blob_source = {'source': {'image_uri': blob_uri}} print('Analyzing %s.' % file_name) @@ -59,8 +51,6 @@ def blur_offensive_images(data, context): # [START functions_imagemagick_blur] # Blurs the given file using ImageMagick. def __blur_image(blob): - print(blob) - file_name = blob.name temp_local_filename = '/tmp/%s' % os.path.basename(file_name) From ae0ccb8883485e3507bb9b3e2b5178686cfbaf72 Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Wed, 12 Sep 2018 14:34:18 -0700 Subject: [PATCH 5/8] Address comments, pt 2 Change-Id: I465cb4531a024265f66f857cbd793949b597d252 --- functions/imagemagick/main.py | 13 ++++----- functions/imagemagick/main_test.py | 37 ++++---------------------- functions/imagemagick/requirements.txt | 1 + 3 files changed, 11 insertions(+), 40 deletions(-) diff --git a/functions/imagemagick/main.py b/functions/imagemagick/main.py index 71fcf603bce..a0c6e6f5fdb 100644 --- a/functions/imagemagick/main.py +++ b/functions/imagemagick/main.py @@ -15,10 +15,9 @@ # [START functions_imagemagick_setup] import os -import subprocess from google.cloud import storage, vision - +from wand.image import Image storage_client = storage.Client() vision_client = vision.ImageAnnotatorClient() @@ -59,12 +58,10 @@ def __blur_image(blob): print('Image %s was downloaded to %s.' % (file_name, temp_local_filename)) # Blur the image using ImageMagick. - subprocess.check_call([ - 'convert', temp_local_filename, - '-channel', 'RGBA', - '-blur', '0x24', - temp_local_filename - ]) + with Image(filename=temp_local_filename) as image: + image.resize(*image.size, blur=16, filter='hamming') + image.save(filename=temp_local_filename) + print('Image %s was blurred.' % file_name) # Upload the Blurred image back into the bucket. diff --git a/functions/imagemagick/main_test.py b/functions/imagemagick/main_test.py index 82d5f731224..727bd17e497 100644 --- a/functions/imagemagick/main_test.py +++ b/functions/imagemagick/main_test.py @@ -23,32 +23,6 @@ class DictObject(dict): pass -@patch('main.__blur_image') -def test_do_nothing_on_delete(__blur_image, capsys): - __blur_image = MagicMock() - data = { - 'resource_state': 'not_exists', - 'name': '' - } - - main.blur_offensive_images(data, None) - - out, _ = capsys.readouterr() - assert 'This is a deletion event.' in out - assert __blur_image.called is False - - -@patch('main.__blur_image') -def test_do_nothing_on_deploy(__blur_image, capsys): - __blur_image = MagicMock() - - main.blur_offensive_images({}, None) - - out, _ = capsys.readouterr() - assert 'This is a deploy event.' in out - assert __blur_image.called is False - - @patch('main.__blur_image') @patch('main.vision_client') @patch('main.storage_client') @@ -65,7 +39,6 @@ def test_process_offensive_image( filename = str(uuid.uuid4()) data = { - 'resource_state': '', 'bucket': 'my-bucket', 'name': filename } @@ -94,7 +67,6 @@ def test_process_safe_image( filename = str(uuid.uuid4()) data = { - 'resource_state': '', 'bucket': 'my-bucket', 'name': filename } @@ -108,15 +80,16 @@ def test_process_safe_image( @patch('main.os') -@patch('main.subprocess') -def test_blur_image(subprocess_mock, os_mock, capsys): +@patch('main.Image') +def test_blur_image(image_mock, os_mock, capsys): filename = str(uuid.uuid4()) os_mock.remove = MagicMock() os_mock.path = MagicMock() os_mock.path.basename = MagicMock(side_effect=(lambda x: x)) - subprocess_mock.check_call = MagicMock() + image_mock.return_value = image_mock + image_mock.__enter__.return_value = image_mock blob = DictObject() blob.name = filename @@ -131,4 +104,4 @@ def test_blur_image(subprocess_mock, os_mock, capsys): assert 'Image %s was blurred.' % filename in out assert 'Blurred image was uploaded to %s.' % filename in out assert os_mock.remove.called - assert subprocess_mock.check_call.called + assert image_mock.resize.called diff --git a/functions/imagemagick/requirements.txt b/functions/imagemagick/requirements.txt index d2cc1607630..7d5b2930623 100644 --- a/functions/imagemagick/requirements.txt +++ b/functions/imagemagick/requirements.txt @@ -1,2 +1,3 @@ google-cloud-vision==0.33.0 google-cloud-storage==1.11.0 +Wand==0.4.4 From e5dbac066ab51c9e03f01cb34109481356fd9976 Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Wed, 12 Sep 2018 14:41:25 -0700 Subject: [PATCH 6/8] Use UserDict instead of DictObject Change-Id: Ie69ce88686481640da59207682b078b800631620 --- functions/imagemagick/main_test.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/functions/imagemagick/main_test.py b/functions/imagemagick/main_test.py index 727bd17e497..fb5e9356987 100644 --- a/functions/imagemagick/main_test.py +++ b/functions/imagemagick/main_test.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from collections import UserDict import uuid from mock import MagicMock, patch @@ -19,10 +20,6 @@ import main -class DictObject(dict): - pass - - @patch('main.__blur_image') @patch('main.vision_client') @patch('main.storage_client') @@ -31,8 +28,8 @@ def test_process_offensive_image( vision_client, storage_client, capsys): - result = DictObject() - result.safe_search_annotation = DictObject() + result = UserDict() + result.safe_search_annotation = UserDict() result.safe_search_annotation.adult = 5 result.safe_search_annotation.violence = 5 vision_client.safe_search_detection = MagicMock(return_value=result) @@ -59,8 +56,8 @@ def test_process_safe_image( vision_client, storage_client, capsys): - result = DictObject() - result.safe_search_annotation = DictObject() + result = UserDict() + result.safe_search_annotation = UserDict() result.safe_search_annotation.adult = 1 result.safe_search_annotation.violence = 1 vision_client.safe_search_detection = MagicMock(return_value=result) @@ -91,7 +88,7 @@ def test_blur_image(image_mock, os_mock, capsys): image_mock.return_value = image_mock image_mock.__enter__.return_value = image_mock - blob = DictObject() + blob = UserDict() blob.name = filename blob.download_to_filename = MagicMock() blob.upload_from_filename = MagicMock() From a2b458c57a0ceb4d46f416266637a104fe23b346 Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Fri, 14 Sep 2018 16:15:43 -0700 Subject: [PATCH 7/8] Address comments Change-Id: I62036d693fcf2451fbd1129d6799a736852ea455 --- functions/imagemagick/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/functions/imagemagick/main.py b/functions/imagemagick/main.py index a0c6e6f5fdb..e7797b2030f 100644 --- a/functions/imagemagick/main.py +++ b/functions/imagemagick/main.py @@ -15,6 +15,7 @@ # [START functions_imagemagick_setup] import os +import tempfile from google.cloud import storage, vision from wand.image import Image @@ -51,7 +52,7 @@ def blur_offensive_images(data, context): # Blurs the given file using ImageMagick. def __blur_image(blob): file_name = blob.name - temp_local_filename = '/tmp/%s' % os.path.basename(file_name) + _, temp_local_filename = tempfile.mkstemp() # Download file from bucket. blob.download_to_filename(temp_local_filename) From ca26a772f0f302b4383ea10064b6ffd5da66cf8c Mon Sep 17 00:00:00 2001 From: Ace Nassri Date: Tue, 25 Sep 2018 17:31:13 -0700 Subject: [PATCH 8/8] Use format strings + make function idempotent Change-Id: Iaddf913fd9be99e7daff6caf45aac2a03c1dceeb --- functions/imagemagick/main.py | 37 +++++++++++++++++++----------- functions/imagemagick/main_test.py | 17 ++++++++------ 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/functions/imagemagick/main.py b/functions/imagemagick/main.py index e7797b2030f..1fe57d09c1a 100644 --- a/functions/imagemagick/main.py +++ b/functions/imagemagick/main.py @@ -31,43 +31,54 @@ def blur_offensive_images(data, context): file_data = data file_name = file_data['name'] - blob = storage_client.bucket(file_data['bucket']).get_blob(file_name) - blob_uri = 'gs://%s/%s' % (file_data['bucket'], file_name) + bucket_name = file_data['bucket'] + + blob = storage_client.bucket(bucket_name).get_blob(file_name) + blob_uri = f'gs://{bucket_name}/{file_name}' blob_source = {'source': {'image_uri': blob_uri}} - print('Analyzing %s.' % file_name) + # Ignore already-blurred files + if file_name.startswith('blurred-'): + print(f'The image {file_name} is already blurred.') + return + + print(f'Analyzing {file_name}.') result = vision_client.safe_search_detection(blob_source) detected = result.safe_search_annotation + # Process image if detected.adult == 5 or detected.violence == 5: - print('The image %s was detected as inappropriate.' % file_name) + print(f'The image {file_name} was detected as inappropriate.') return __blur_image(blob) else: - print('The image %s was detected as OK.' % file_name) + print(f'The image {file_name} was detected as OK.') # [END functions_imagemagick_analyze] # [START functions_imagemagick_blur] # Blurs the given file using ImageMagick. -def __blur_image(blob): - file_name = blob.name +def __blur_image(current_blob): + file_name = current_blob.name _, temp_local_filename = tempfile.mkstemp() # Download file from bucket. - blob.download_to_filename(temp_local_filename) - print('Image %s was downloaded to %s.' % (file_name, temp_local_filename)) + current_blob.download_to_filename(temp_local_filename) + print(f'Image {file_name} was downloaded to {temp_local_filename}.') # Blur the image using ImageMagick. with Image(filename=temp_local_filename) as image: image.resize(*image.size, blur=16, filter='hamming') image.save(filename=temp_local_filename) - print('Image %s was blurred.' % file_name) + print(f'Image {file_name} was blurred.') - # Upload the Blurred image back into the bucket. - blob.upload_from_filename(temp_local_filename) - print('Blurred image was uploaded to %s.' % file_name) + # Send Blurred image back to the bucket (with a 'blurred-' prefix). + # The prefix is necessary to avoid re-invoking the function upon upload. + new_file_name = f'blurred-{file_name}' + new_blob = current_blob.bucket.blob(new_file_name) + new_blob.upload_from_filename(temp_local_filename) + print(f'Blurred image was uploaded to {new_file_name}.') # Delete the temporary file. os.remove(temp_local_filename) diff --git a/functions/imagemagick/main_test.py b/functions/imagemagick/main_test.py index fb5e9356987..1afc25337ce 100644 --- a/functions/imagemagick/main_test.py +++ b/functions/imagemagick/main_test.py @@ -24,9 +24,9 @@ @patch('main.vision_client') @patch('main.storage_client') def test_process_offensive_image( - __blur_image, - vision_client, storage_client, + vision_client, + __blur_image, capsys): result = UserDict() result.safe_search_annotation = UserDict() @@ -52,9 +52,9 @@ def test_process_offensive_image( @patch('main.vision_client') @patch('main.storage_client') def test_process_safe_image( - __blur_image, - vision_client, storage_client, + vision_client, + __blur_image, capsys): result = UserDict() result.safe_search_annotation = UserDict() @@ -71,6 +71,7 @@ def test_process_safe_image( main.blur_offensive_images(data, None) out, _ = capsys.readouterr() + assert 'Analyzing %s.' % filename in out assert 'The image %s was detected as OK.' % filename in out assert __blur_image.called is False @@ -90,6 +91,8 @@ def test_blur_image(image_mock, os_mock, capsys): blob = UserDict() blob.name = filename + blob.bucket = UserDict() + blob.bucket.blob = MagicMock(return_value=blob) blob.download_to_filename = MagicMock() blob.upload_from_filename = MagicMock() @@ -97,8 +100,8 @@ def test_blur_image(image_mock, os_mock, capsys): out, _ = capsys.readouterr() - assert 'Image %s was downloaded to /tmp/%s.' % (filename, filename) in out - assert 'Image %s was blurred.' % filename in out - assert 'Blurred image was uploaded to %s.' % filename in out + assert f'Image {filename} was downloaded to' in out + assert f'Image {filename} was blurred.' in out + assert f'Blurred image was uploaded to blurred-{filename}.' in out assert os_mock.remove.called assert image_mock.resize.called