使用 Amazon S3 Object Lambda 在擷取影像時動態加上浮水印影像

教學

概觀

透過 Amazon S3 Object Lambda,您可將自己的程式碼新增至 S3 GET、HEAD 和 LIST 請求,以便在資料傳回應用程式時對其做出修改。您可以使用自訂程式碼修改 S3 GET 請求傳回的資料,以轉換資料格式 (例如,XML 轉換為 JSON)、動態調整影像大小、修訂機密資料以及更多動作。您也可以使用 S3 Object Lambda 修改 S3 LIST 請求的輸出,以建立儲存貯體中物件的自訂檢視,以及用於修改物件名稱和大小等物件中繼資料的 S3 HEAD 請求。

本教學的目的是向您展示如何開始使用 Amazon S3 Object Lambda。許多組織在 Amazon S3 中存放可由不同應用程式存取的影像,每個影像都有獨特的資料格式要求。在某些情況下,影像可能需要修改才能包含浮水印,視存取影像的使用者而定 (例如,付費訂閱用戶可以檢視沒有浮水印的影像,而非付費使用者則會收到有浮水印的影像)。

在本教學中,我們將使用 S3 Object Lambda 在從 Amazon S3 擷取影像時將浮水印加入影像。S3 Object Lambda 可用來在從 Amazon S3 擷取資料時修改資料,而無需變更現有物件或維護資料的多個衍生副本。透過呈現相同資料的多個檢視並且不需要儲存衍生副本,您可以節省儲存成本。

您會完成的目標

在本教學中,您將:

  • 建立 Amazon S3 儲存貯體
  • 建立 S3 存取點
  • 建立 AWS Lambda 函數以修改影像
  • 建立 S3 Object Lambda 存取點

先決條件

您需要 AWS 帳戶才能完成本教學。存取此支援頁面,瞭解有關如何建立和啟用新 AWS 帳戶的詳細資訊。

您可以為本教學建立 IAM 使用者,也可以為現有的 IAM 使用者新增許可。若要完成本教學,您的 IAM 使用者必須包含下列許可,才能存取相關 AWS 資源並執行特定動作:

  • s3:CreateBucket
  • s3:PutObject
  • s3:GetObject
  • s3:ListBucket
  • s3:CreateAccessPoint
  • s3:CreateAccessPointForObjectLambda
  • s3-object-lambda:WriteGetObjectResponse
  • lambda:CreateFunction
  • lambda:InvokeFunction
  • iam:AttachRolePolicy
  • iam:CreateRole
  • iam:PutRolePolicy

若要清理您在本教學中建立的資源,您將需要下列 IAM 許可:

  • s3:DeleteBucket
  • s3:DeleteAccessPoint
  • s3:DeleteAccessPointForObjectLambda
  • lambda:DeleteFunction
  • iam:DeleteRole

 

 AWS 經驗

初階

 完成時間

20 分鐘

 完成成本

低於 1 美元 (Amazon S3 定價頁面)

 要求

AWS 帳戶*

**過去 24 小時內建立的帳戶可能尚未具有本教學所需資源的存取權。

 使用的服務

 上次更新日期

2023 年 2 月 1 日

先決條件

您需要 AWS 帳戶才能完成本教學。存取此支援頁面,瞭解有關如何建立和啟用新 AWS 帳戶的詳細資訊。

您可以為本教學建立 IAM 使用者,也可以為現有的 IAM 使用者新增許可。若要完成本教學,您的 IAM 使用者必須包含下列許可,才能存取相關 AWS 資源並執行特定動作: 

實作

步驟 1:建立 Amazon S3 儲存貯體

1.1 – 登入 Amazon S3 主控台

1.2 – 建立 S3 儲存貯體

  • 從左側導覽窗格的 Amazon S3 功能表中選取儲存貯體,然後選擇建立儲存貯體按鈕。

1.3

  • 儲存貯體名稱欄位中,輸入儲存貯體的全域唯一描述性名稱。選取您想在其中建立儲存貯體的 AWS 區域。我們將在稍後的教學中建立另一個必須位於相同 AWS 區域的資源。
  • 至於剩餘的選項,使用預設選項即可。導覽至頁面底部,然後選擇建立儲存貯體

步驟 2:上傳物件

現在您的儲存貯體已建立且設定完成,您已經準備好上傳影像了。

2.1 – 上傳物件

  • 從可用儲存貯體清單中,選取您剛剛建立的儲存貯體名稱。

2.2

  • 接下來,請確定已選取物件分頁。然後在物件區段中,選擇上傳按鈕。

2.3 – 新增檔案

  • 選擇新增檔案按鈕,然後從檔案瀏覽器中選取您要上傳的影像。
  • 如果您想要的話,您可以上傳這張範例影像。

2.4 – 上傳

  • 向下導覽頁面,然後選擇上傳按鈕。

2.5

  • 上傳完成且顯示成功後,請選擇關閉按鈕。

步驟 3:建立 S3 存取點

建立將用於支援 S3 Object Lambda 存取點的 Amazon S3 存取點,我們將在稍後的教學中建立這個存取點。

3.1 — 建立 S3 存取點

  • 導覽至 S3 主控台,然後選取左側導覽窗格中的存取點功能表選項。然後,選擇建立存取點按鈕。

3.2

  • 屬性區段中,輸入所需的存取點名稱,接著選取瀏覽 S3按鈕來選擇您在步驟 1 中輸入的儲存貯體名稱。接下來,將網路原點設為網際網路

3.3

  • 保留所有其他預設值。瀏覽至頁面底部,然後選擇建立存取點按鈕。

3.4

  • 當您導覽至左側導覽窗格中的存取點時,S3 存取點現在會出現在清單中。

步驟 4:建立 Lambda 函數

  • 接下來,建立 Lambda 函數,該函數會在透過 S3 Object Lambda 存取點提出 S3 GET 請求時調用。
  • 我們會使用 AWS 管理主控台中的 AWS CloudShell 來建置和測試 S3 Object Lambda。如果您符合以下要求,則可以使用自己的電腦或 AWS Cloud9 執行個體來建置解決方案:
    - 最新版的 AWS Command Line Interface (CLI)
    - 用來建立 AWS Lambda 函數/層和 IAM 角色的憑證
    - Python 3.9
    - 壓縮工具程式
    - jq 工具程式

4.1 — 啟動 CloudShell 終端

選取 AWS 管理主控台右上方功能表中的 CloudShell 圖示。

如果出現 CloudShell 介紹視窗,敬請閱讀內容並選擇關閉

瀏覽器會開啟一個具有 CloudShell 終端的分頁 (類似下列螢幕截圖):

4.2 — 準備部署 Lambda 函數

  • 在 CloudShell 中執行下列程式碼以準備環境,並使用 Pillow 模組部署 Lambda 層。將下列程式碼複製並貼上 CloudShell 中,以安裝所需的相依項並部署 Lambda 函數。
# Install the required libraries to build new python
sudo yum install gcc openssl-devel bzip2-devel libffi-devel -y
# Install Pyenv
curl https://pyenv.run | bash
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
source ~/.bash_profile

# Install Python version 3.9
pyenv install 3.9.13
pyenv global 3.9.13

# Build the pillow Lambda layer
mkdir python
cd python
pip install pillow -t .
cd ..
zip -r9 pillow.zip python/
aws lambda publish-layer-version \
    --layer-name Pillow \
    --description "Python Image Library" \
    --license-info "HPND" \
    --zip-file fileb://pillow.zip \
    --compatible-runtimes python3.9

附註:複製和貼上程式碼時,CloudShell 會打開一個警告視窗,確認您要貼上多行程式碼。選取貼上

此步驟可能需要 10 到 15 分鐘才能完成。

4.3 – 建置 Lambda 函數

  • 下載 TrueType 字型,此字型將由 Lambda 函數用來將浮水印新增至影像。將以下命令複製並貼上 CloudShell 中。
wget https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/branding/Amazon_Typefaces_Complete_Font_Set_Mar2020.zip
  • 擷取 TrueType 字型,此字型將會用來在影像中寫入浮水印文字。
unzip -oj Amazon_Typefaces_Complete_Font_Set_Mar2020.zip "Amazon_Typefaces_Complete_Font_Set_Mar2020/Ember/AmazonEmber_Rg.ttf"
  • 建立將用來處理 S3 Object Lambda 請求的 Lambda 程式碼。
cat << EOF > lambda.py
import boto3
import json
import os
import logging
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
from urllib import request
from urllib.parse import urlparse, parse_qs, unquote
from urllib.error import HTTPError
from typing import Optional

logger = logging.getLogger('S3-img-processing')
logger.addHandler(logging.StreamHandler())
logger.setLevel(getattr(logging, os.getenv('LOG_LEVEL', 'INFO')))
FILE_EXT = {
    'JPEG': ['.jpg', '.jpeg'],
    'PNG': ['.png'],
    'TIFF': ['.tif']
}
OPACITY = 64  # 0 = transparent and 255 = full solid


def get_img_encoding(file_ext: str) -> Optional[str]:
    result = None
    for key, value in FILE_EXT.items():
        if file_ext in value:
            result = key
            break
    return result


def add_watermark(img: Image, text: str) -> Image:
    font = ImageFont.truetype("AmazonEmber_Rg.ttf", 82)
    txt = Image.new('RGBA', img.size, (255, 255, 255, 0))
    if img.mode != 'RGBA':
        image = img.convert('RGBA')
    else:
        image = img

    d = ImageDraw.Draw(txt)
    # Positioning Text
    width, height = image.size
    text_width, text_height = d.textsize(text, font)
    x = width / 2 - text_width / 2
    y = height / 2 - text_height / 2
    # Applying Text
    d.text((x, y), text, fill=(255, 255, 255, OPACITY), font=font)
    # Combining Original Image with Text and Saving
    watermarked = Image.alpha_composite(image, txt)
    return watermarked


def handler(event, context) -> dict:
    logger.debug(json.dumps(event))
    object_context = event["getObjectContext"]
    # Get the presigned URL to fetch the requested original object
    # from S3
    s3_url = object_context["inputS3Url"]
    # Extract the route and request token from the input context
    request_route = object_context["outputRoute"]
    request_token = object_context["outputToken"]
    parsed_url = urlparse(event['userRequest']['url'])
    object_key = parsed_url.path
    logger.info(f'Object to retrieve: {object_key}')
    parsed_qs = parse_qs(parsed_url.query)
    for k, v in parsed_qs.items():
        parsed_qs[k][0] = unquote(v[0])

    filename = os.path.splitext(os.path.basename(object_key))
    # Get the original S3 object using the presigned URL
    req = request.Request(s3_url)
    try:
        response = request.urlopen(req)
    except HTTPError as e:
        logger.info(f'Error downloading the object. Error code: {e.code}')
        logger.exception(e.read())
        return {'status_code': e.code}

    if encoding := get_img_encoding(filename[1].lower()):
        logger.info(f'Compatible Image format found! Processing image: {"".join(filename)}')
        img = Image.open(response)
        logger.debug(f'Image format: {img.format}')
        logger.debug(f'Image mode: {img.mode}')
        logger.debug(f'Image Width: {img.width}')
        logger.debug(f'Image Height: {img.height}')

        img_result = add_watermark(img, parsed_qs.get('X-Amz-watermark', ['Watermark'])[0])
        img_bytes = BytesIO()

        if img.mode != 'RGBA':
            # Watermark added an Alpha channel that is not compatible with JPEG. We need to convert to RGB to save
            img_result = img_result.convert('RGB')
            img_result.save(img_bytes, format='JPEG')
        else:
            # Will use the original image format (PNG, GIF, TIFF, etc.)
            img_result.save(img_bytes, encoding)
        img_bytes.seek(0)
        transformed_object = img_bytes.read()

    else:
        logger.info(f'File format not compatible. Bypass file: {"".join(filename)}')
        transformed_object = response.read()

    # Write object back to S3 Object Lambda
    s3 = boto3.client('s3')
    # The WriteGetObjectResponse API sends the transformed data
    if os.getenv('AWS_EXECUTION_ENV'):
        s3.write_get_object_response(
            Body=transformed_object,
            RequestRoute=request_route,
            RequestToken=request_token)
    else:
        # Running in a local environment. Saving the file locally
        with open(f'myImage{filename[1]}', 'wb') as f:
            logger.debug(f'Writing file: myImage{filename[1]} to the local filesystem')
            f.write(transformed_object)

    # Exit the Lambda function: return the status code
    return {'status_code': 200}
EOF
  • 建立包含 Python 程式碼和 TrueType 字型檔案的 Lambda 壓縮檔案。
zip -r9 lambda.zip lambda.py AmazonEmber_Rg.ttf
  • 建立附加至 Lambda 函數的 IAM 角色。
aws iam create-role --role-name ol-lambda-images --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{"Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'
  • 將預先定義的 IAM 政策附加到先前建立的 IAM 角色。此政策包含執行 Lambda 函數所需的最低許可。
aws iam attach-role-policy --role-name ol-lambda-images --policy-arn arn:aws:iam::aws:policy/service-role/AmazonS3ObjectLambdaExecutionRolePolicy

export OL_LAMBDA_ROLE=$(aws iam get-role --role-name ol-lambda-images | jq -r .Role.Arn)

export LAMBDA_LAYER=$(aws lambda list-layers --query 'Layers[?contains(LayerName, `Pillow`) == `true`].LatestMatchingVersion.LayerVersionArn' | jq -r .[])
  • 建立並上傳 Lambda 函數。
aws lambda create-function --function-name ol_image_processing \
 --zip-file fileb://lambda.zip --handler lambda.handler --runtime python3.9 \
 --role $OL_LAMBDA_ROLE \
 --layers $LAMBDA_LAYER \
 --memory-size 1024

步驟 5:建立 S3 Object Lambda 存取點

建立用於存取 S3 儲存貯體中存放之影像的 S3 Object Lambda 存取點。

5.1 — 建立 S3 Object Lambda 存取點

一般區段中,對於 Object Lambda 存取點名稱,輸入 ol-amazon-s3-images-guide。

確保 S3 Object Lambda 存取點的 AWS 區域與您在步驟 1.3 中建立 S3 儲存貯體時指定的 AWS 區域相符。

對於支援的存取點,使用瀏覽 S3 按鈕指定您在步驟 3.2 中建立的 S3 存取點的 Amazon Resource Name (ARN)。

向下導覽以檢視轉換組態。在 S3 API 清單中,選取 GetObject 選項。

Lambda 函數下,指定 ol_image_processing。

接下來,導覽至頁面底部,然後選擇建立 Object Lambda 存取點。

步驟 6:從 S3 Object Lambda 存取點下載影像

建立 S3 物件 Lambda 存取點之後,我們將開啟影像以確認浮水印已在請求期間正確加入。

6.1 — 開啟 S3 Object Lambda 存取點

  • 在 S3 主控台左側導覽窗格中選擇 Object Lambda 存取點以返回 S3 Object Lambda 存取點清單,然後選取您在步驟 5.1 中建立的 S3 Object Lambda 存取點。在此範例中,我們選擇 S3 Object Lambda 存取點做為 ol-amazon-s3-images-guide。

選取您在步驟 2.4 中上傳的影像,然後選擇開啟按鈕。

瀏覽器會開啟新的分頁,其中包含您的影像和浮水印。
 
從 S3 Object Lambda 存取點下載的所有相容影像現在都會包含浮水印文字。


6.2 — 從 AWS CLI 下載轉換後的影像

  • 您也可以使用 AWS CLI 下載影像。若要這麼做,您需要 S3 Object Lambda 存取點的 Amazon Resource Name (ARN)。在 S3 主控台中,導覽至 Object Lambda 存取點頁面,選取 S3 Object Lambda 存取點的名稱,接著選取屬性分頁,然後選擇 Amazon Resource Name (ARN) 下方的複製圖示。

6.3 — 從 CloudShell 執行 AWS CLI 命令

在 CloudShell 瀏覽器分頁中,輸入下列內容:

aws s3api get-object --bucket <paste the ARN copied above here> --key <image filename here> <filename to write here>

6.4 — 將影像下載到本機電腦

在 CloudShell 中,選擇右上角的動作,然後選取下載檔案。

從 S3 Object Lambda 存取點下載影像時,輸入您在步驟 6.3 中定義的檔案名稱,然後選擇下載

現在,您可以從本機電腦打開影像。

附註:影像檢視程式可能依電腦和作業系統而異。如果您不確定要使用哪個應用程式來開啟影像,請洽詢您的管理員。

步驟 7:清除資源

接下來,您將清除在本教學中建立的資源。最佳實務是刪除不再使用的資源,以免產生意外費用。

7.1 — 刪除 S3 Object Lambda 存取點

  • 導覽至 S3 主控台,然後在左側導覽窗格中選擇 Object Lambda 存取點
  • Object Lambda 存取點頁面上,選擇您在步驟 5.1 中建立的 S3 Object Lambda 存取點左側的選項按鈕。

選擇刪除

在出現的文字欄位中,輸入 S3 Object Lambda 存取點的名稱,確認您要刪除 S3 Object Lambda 存取點,然後選擇刪除

7.2 — 刪除 S3 存取點

  • S3 主控台的左側導覽窗格中,選擇存取點。
  • 導覽至您在步驟 3.1 中建立的 S3 存取點,然後選擇 S3 存取點名稱旁邊的選項按鈕。
  • 選擇刪除

在出現的文字欄位中輸入存取點的名稱,以確認您要刪除存取點,然後選擇刪除

7.3 – 刪除測試物件

  • 導覽至 S3 主控台,然後選取左側導覽窗格中的儲存貯體功能表選項。首先,您需要從測試儲存貯體中刪除測試物件。選取您在本教學中使用的儲存貯體名稱。
  • 選取測試物件名稱左側的核取方塊,然後選擇刪除按鈕。
  • 刪除物件頁面上,確認您是否已選取要刪除的物件,並在永久刪除物件確認方塊中輸入刪除。然後,選擇刪除物件按鈕以繼續。
接下來,您將看到一條橫幅,說明刪除是否成功。

7.4 — 刪除 S3 儲存貯體

  • 接下來,在左側導覽窗格的 S3 主控台功能表中選擇儲存貯體。選取為本教學建立的來源儲存貯體左側的選項按鈕,然後選擇刪除按鈕。

檢閱警告訊息。如果要繼續刪除此儲存貯體,請在刪除儲存貯體確認方塊中輸入儲存貯體的名稱,然後選擇刪除儲存貯體。

7.5 – 刪除 Lambda 函數

  • AWS Lambda 主控台中,選擇左側導覽窗格的函數
  • 選取您在步驟 4.3 中建立之函數名稱左側的核取方塊。
  • 選擇動作,然後選擇刪除。在刪除函數對話方塊中,選擇「刪除」。

結語

恭喜您! 您學會了如何使用 Amazon S3 Object Lambda 在擷取影像時動態新增浮水印至影像,並將處理過的影像傳送回請求的用戶端。您可以針對您的使用案例自訂 Lambda 函數,以修改 S3 GET、HEAD 和 LIST 請求所傳回的資料,包含使用呼叫者特定詳細資料的自訂浮水印、遮罩敏感資料、篩選特定資料列、使用其他資料庫的資訊增強資料、轉換資料格式等等。

本頁對您是否有幫助?

後續步驟