V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
scf10cent
V2EX  ›  推广

如何用 Serverless 优雅地给网站图片加水印

  •  
  •   scf10cent · 2020-03-08 20:43:13 +08:00 · 1190 次点击
    这是一个创建于 1724 天前的主题,其中的信息可能已经有所发展或是发生改变。

    很多论坛、博客在进行图片上传之后,都会给自己的图像加上水印,这样可以证明这张图片「属于我」或者是「来自我的博客 /网站」。那么使用 Serverless 技术来加水印的方法比传统方法好在哪儿呢,本文将对此进行一个简单的分享。

    传统的加水印的方法,通常是在流程内进行。即:

    传统方法

    这种做法虽然可行,但是无疑会增加单次请求,服务端的压力,如果是高并发的情况下,或者多人上传多张大图的时候,那么可能就会造成自身服务器资源效果过大。

    如果在加水印过程中失败,就有可能导致图像存储失败,致使数据丢失,并不理智。所以后来有人做了如下改进:

    传统方法改进版

    这样做法的好处就是 —— 我们可以快速将图片存储,存储之后通过一个单独处理的线程,对任务列表进行处理,这样一方面是可以保证我们马上把用户上传的图片存储,并且可以显示,同时也可以在后台进行水印处理,待处理之后,再将图片覆盖或者单独存储,用户如果需要读取图片时,可以自动变为已经水印后的图片。

    这种做法相对于前者来说可能稍微复杂一些,但是实际上却是在数据上更加稳定,服务端压力更小,更加可控的一种操作。但是,这整个流程还是要在自己的服务器上做完,现在已经有很多人将图片等资源存储到腾讯云的 对象存储( COS )中,那么可不可以通过某些 COS 触发器与云函数 SCF 结合,实现一个不在自己服务器加水印的流程呢?

    本文将以腾讯云函数 SCF 的函数模板( Python 语言)为例,进行一个简单的分享。

    ▎实验

    新建函数

    在无服务器云函数中,选择模板函数:

    新建函数

    通过搜索「图像」关键词,选中图像压缩,并且确定建立。保存之后,点击函数代码,进行代码编写。

    编写代码

    COS 触发器

    有些人可能对 COS 触发器还不是很了解,此时可以点击配置,来熟悉 COS 触发器样式:

    触发器

    可以看到如下:

    {
       "Records":[
          {
            "event": {
              "eventVersion":"1.0",
              "eventSource":"qcs::cos",
              "eventName":"cos: ObjectCreated: *",
              "eventTime":1501054710,
              "eventQueue":"qcs:0:cos:gz:1251111111:cos",
              "requestParameters":{
                "requestSourceIP": "111.111.111.111",
                "requestHeaders":{
                  "Authorization": "上传的鉴权信息"
                }
              }
             },
             "cos":{
                "cosSchemaVersion":"1.0",
                "cosNotificationId":"设置的或返回的 ID",
                "cosBucket":{
                   "name":"bucketname",
                   "appid":"appId",
                   "region":"gz"
                },
                "cosObject":{
                   "key":"/appid/bucketname/DSC_0002.JPG",
                   "size":2598526,
                   "meta":{
                     "Content-Type": "text/plain",
                     "x-cos-meta-test": "自定义的 meta",
                     "x-image-test": "自定义的 meta"
                   },
                   "url": "访问文件的源站 url"
                }
             }
          }
       ]
    }
    

    这里面可以看到整个一个数据结构,需要注意 Records 是一个数组格式,其次就是:

    "cosBucket":{"name":"bucketname","appid":"appId","region":"gz"}

    这里面是由该 bucket 触发

    "cosObject":{"key":"/appid/bucketname/DSC_0002.JPG","size":2598526,"meta":{"Content-Type":"text/plain","x-cos-meta-test":"自定义的 meta","x-image-test":"自定义的 meta"},"url":"访问文件的源站 url"}

    这里面的 key 是在上述 bucket 中新建的文件名字。

    所以,我们可以按照我们的实际情况,将上面的内容简单修改一下,成为我们格式,以便测试(在生产中,这个是自动生成的触发格式,并不需要我们修改,我们修改只是为了测试.

    {
       "Records":[
          {
            "event": {
              "eventVersion":"1.0",
              "eventSource":"qcs::cos",
              "eventName":"cos: ObjectCreated: *",
              "eventTime":1501054710,
              "eventQueue":"qcs:0:cos:gz:1251111111:cos",
              "requestParameters":{
                "requestSourceIP": "111.111.111.111",
                "requestHeaders":{
                  "Authorization": "上传的鉴权信息"
                }
              }
             },
             "cos":{
                "cosSchemaVersion":"1.0",
                "cosNotificationId":"设置的或返回的 ID",
                "cosBucket":{
                   "name":"mytestcos",
                   "appid":"appId",
                   "region":"gz"
                },
                "cosObject":{
                   "key":"test.png",
                   "size":2598526,
                   "meta":{
                     "Content-Type": "text/plain",
                     "x-cos-meta-test": "自定义的 meta",
                     "x-image-test": "自定义的 meta"
                   },
                   "url": "访问文件的源站 url"
                }
             }
          }
       ]
    }
    

    这里主要修改了我的 cosBucket-name: mytestcos,以及 key: test.png

    cos 修改

    代码修改

    之所以使用现有的模板,是因为该模板的有 PIL 和 qcloud_cos_v5 等相关 package,而这两个 package 正是我们即将需要的,这样就可以省去我们打包函数的流程,只需要进行简单修改即可实现。

    添加水印:

    def add_word(pic_path, save_path):
        # 打开图片
        im = Image.open(pic_path).convert('RGBA')
        # 新建一个空白图片,尺寸与打开图片一样
        txt = Image.new('RGBA', im.size, (0, 0, 0, 0))
        # 设置字体
        fnt = ImageFont.truetype("/tmp/font.ttf", 40)
        # 操作新建的空白图片>>将新建的图片添入画板
        d = ImageDraw.Draw(txt)
        # 在新建的图片上添加字体
        d.text((txt.size[0] - 220, txt.size[1] - 80), "By Dfounder", font=fnt,  fill=(255, 255, 255, 255))
        # 合并两个图片
        out = Image.alpha_composite(im, txt)
        # 保存图像
        out.save(save_path)
    

    在添加水印的时候,我们设置的是文字水印,所以需要设置字体和字号:

    fnt = ImageFont.truetype("/tmp/font.ttf",40)

    此时,我们需要在执行之前,先将字体文件传入到 /tmp/ 文件夹下:

    response = client.get_object(Bucket="mytestcos-12567****", Key="font.ttf", ) response['Body'].get_stream_to_file('/tmp/font.ttf')

    以我的 cos 为例:

    例子

    然后,接下来就是对触发的 event 进行解析,包括获得新建的图像名称,从 COS 拉取,放到本地,然后进行水印等,再上传回新的 COS 中:

    for record in event['Records']:
            try:
                bucket = record['cos']['cosBucket']['name'] + '-' + str(appid)
                key = record['cos']['cosObject']['key']
                key = key.replace('/' + str(appid) + '/' + record['cos']['cosBucket']['name'] + '/', '', 1)
                download_path = '/tmp/{}'.format(key)
                upload_path = '/tmp/new_pic-{}'.format(key)
    
                # 下载图片
                try:
                    response = client.get_object(Bucket=bucket, Key=key, )
                    response['Body'].get_stream_to_file(download_path)
                except CosServiceError as e:
                    print(e.get_error_code())
                    print(e.get_error_msg())
                    print(e.get_resource_location())
    
                # 图像增加水印
                add_word(download_path, upload_path)
    
    
                # 图像上传
                response = client.put_object_from_local_file(
                    Bucket=to_bucket,
                    LocalFilePath=upload_path.decode('utf-8'),
                    Key=("upload-" + key).decode('utf-8')
                )
    
            except Exception as e:
                print(e)
    

    此处说明一下,为什么要有两个存储桶?

    因为我们要把一个存储桶作为触发 SCF 函数的「工具」,如果我们将水印结果再次写回这个存储桶,在不进行额外判断和处理的前提下,那么这个水印后的图片会再次水印,反反复复造成恶劣的循环,所以此处建立两个存储桶,可以降低难度,也可以保护性能,减少 BUG 诞生。

    完整代码如下:

    # -*- coding: utf-8 -*-
    
    from PIL import Image, ImageFont, ImageDraw
    from qcloud_cos_v5 import CosConfig
    from qcloud_cos_v5 import CosS3Client
    from qcloud_cos_v5 import CosServiceError
    from qcloud_cos_v5 import CosClientError
    
    appid = **  # 请替换为您的 APPID
    secret_id = ***'  # 请替换为您的 SecretId
    secret_key = **'  # 请替换为您的 SecretKey
    region = u'ap-chengdu'  # 请替换为您 bucket 所在的地域
    token = ''
    to_bucket = 'tobucket-12567***'  # 请替换为您用于存放压缩后图片的 bucket
    
    config = CosConfig(Secret_id=secret_id, Secret_key=secret_key, Region=region, Token=token)
    client = CosS3Client(config)
    
    response = client.get_object(Bucket="mytestcos-12567***", Key="font.ttf", )
    response['Body'].get_stream_to_file('/tmp/font.ttf')
    
    def add_word(pic_path, save_path):
        # 打开图片
        im = Image.open(pic_path).convert('RGBA')
        # 新建一个空白图片,尺寸与打开图片一样
        txt = Image.new('RGBA', im.size, (0, 0, 0, 0))
        # 设置字体
        fnt = ImageFont.truetype("/tmp/font.ttf", 40)
        # 操作新建的空白图片>>将新建的图片添入画板
        d = ImageDraw.Draw(txt)
        # 在新建的图片上添加字体
        d.text((txt.size[0] - 220, txt.size[1] - 80), "By Dfounder", font=fnt,  fill=(255, 255, 255, 255))
        # 合并两个图片
        out = Image.alpha_composite(im, txt)
        # 保存图像
        out.save(save_path)
    
    def main_handler(event, context):
        for record in event['Records']:
            try:
                bucket = record['cos']['cosBucket']['name'] + '-' + str(appid)
                key = record['cos']['cosObject']['key']
                key = key.replace('/' + str(appid) + '/' + record['cos']['cosBucket']['name'] + '/', '', 1)
                download_path = '/tmp/{}'.format(key)
                upload_path = '/tmp/new_pic-{}'.format(key)
    
                # 下载图片
                try:
                    response = client.get_object(Bucket=bucket, Key=key, )
                    response['Body'].get_stream_to_file(download_path)
                except CosServiceError as e:
                    print(e.get_error_code())
                    print(e.get_error_msg())
                    print(e.get_resource_location())
    
                # 图像增加水印
                add_word(download_path, upload_path)
    
    
                # 图像上传
                response = client.put_object_from_local_file(
                    Bucket=to_bucket,
                    LocalFilePath=upload_path.decode('utf-8'),
                    Key=("upload-" + key).decode('utf-8')
                )
    
            except Exception as e:
                print(e)
    

    这里面需要注意这几个参数:appid、secret_id、secret_key、to_bucket

    这几个参数的来源如下:

    参数来源

    而 secretid,secretkey 则需要在这里获取:

    id 获取

    测试

    之前我已经上传了一个测试图片在这个 bucket 中,名字是:test.png

    test 图片

    图片是这样子:

    test 图片 2

    然后我们进行一下测试:

    测试

    可以看到,已经测试成功,接下来我们可以去我们的目标 bucket 中看看:

    目标 bucket

    可以看到成功生成了一个图片:

    生成图片

    可以看到图片的右下角,有我们代码中添加的水印:

    水印代码

    至此,我们完成了通过云函数 SCF 为您的网站图片添加水印的基本流程。

    ▎额外想说的

    其实,这篇文章也算是抛砖引玉,大家不仅可以通过这个流程压缩图像、添加水印,甚至还可以对图像进行其他操作。例如图像风格化处理等一些深加工,而这一切过程,都不会占用自身服务器资源,而是通过云函数 SCF 来完成的。

    即使面对高并发,有大量的图片上传时,我们要做的也仅仅是通过 SDK,将图片传入到腾讯云对象存储 COS 中。

    传送门:

    欢迎访问:Serverless 中文网,您可以在 最佳实践 里体验更多关于 Serverless 应用的开发!


    推荐阅读:《 Serverless 架构:从原理、设计到项目实战》

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5747 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 01:35 · PVG 09:35 · LAX 17:35 · JFK 20:35
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.