上传即压缩:华为云 FunctionGraph 图片处理函数
一、背景:图片压缩这种活,最适合事件驱动
网站、App 让用户传图片,原图往往几 MB,直接存、直接展示都费流量费存储。常规做法是"上传时顺手压成小图",但如果在应用服务器里同步压,大图会拖慢上传响应;如果用定时任务扫着压,又有延迟、还漏。
图片压缩是典型的"事件驱动"场景:原图一落到对象存储,就触发一个函数去压,压完写回缩略图,全程异步、不阻塞上传。这和第 25 篇"上传生成缩略图"思路一样,只是这次明着用华为云 FunctionGraph 当计算、OBS 当存储,把"上传即压缩"在托管 Serverless 上跑通。具体函数与触发器配置以官方文档为准,本文讲清链路和压缩代码要点。
二、链路:OBS 事件 → 函数 → 压缩 → 回写
- 用户把原图
photo.jpg上传到 OBS 源桶origin/; - OBS 产生 PutObject 事件,触发器推给 FunctionGraph;
- 函数从源桶下载
photo.jpg到内存; - 用图像处理库(Python 用 Pillow)压到目标质量/尺寸;
- 把压缩图写回目标桶
compressed/; - 返回,链路结束。
全程无服务器常驻:没人传图函数就不跑、不计费;有人传一张就跑一次压一张。比"常驻一台机器专职压缩"省资源。和第 25 篇不同,本文聚焦"压缩"(降质量/降尺寸)而非"缩略图"(只缩小),用途上压缩更省存储和流量。
三、函数里的压缩逻辑
函数运行时把原图读进内存,用 Pillow 压质量/尺寸后回写:
from PIL import Image
import io
def compress(object_body: bytes, max_size=(1280, 1280), quality=80):
img = Image.open(io.BytesIO(object_body))
img.thumbnail(max_size) # 超尺寸则等比缩
buf = io.BytesIO()
# 转 RGB 避免 RGBA 存 JPEG 报错;quality 控制压缩率
img.convert("RGB").save(buf, format="JPEG", quality=quality)
return buf.getvalue()
thumbnail(max_size) 把最长边压到 1280 以内(等比、不变形);quality=80 是 JPEG 压缩质量,80 通常肉眼几乎无差、体积能砍一大半。img.convert("RGB") 很关键——PNG 带透明通道(RGBA)直接存 JPEG 会报错(JPEG 不支持透明),先转 RGB 最稳。压缩结果用 io.BytesIO 存字节,直接上传不落盘(函数临时盘小,且没必要)。Pillow 是否预装、OBS SDK 取/存方法以官方文档为准——有些运行时需把依赖打进函数包。
四、触发与回写:避免循环
和第 25 篇一样,循环触发是头号雷:压缩图写回源桶会再次触发函数、再压、再触发,无限递归。正确做法是写回另一个桶(如 compressed-bucket),源桶触发器只监听 origin/ 前缀,目标桶不挂触发器:
def handler(event, context):
key = event["Records"][0]["s3"]["object"]["key"]
if not key.startswith("origin/"): # 双保险:只处理源前缀
return {"skipped": True}
body = obs_get(src_bucket, key)
out = compress(body)
obs_put(compressed_bucket, "c/" + key.split("/")[-1], out)
return {"ok": True}
if not key.startswith("origin/") 是代码内双保险:即使触发器前缀配错,函数也只处理源文件,杜绝递归把调用次数和费用打爆。压缩图写 compressed_bucket 的 c/ 前缀,和源桶物理隔离,绝不回头触发自己。
五、压缩参数的取舍
第一,quality 不是越低越好。JPEG quality 从 100 往下,体积骤降、画质缓降;到 60~80 之间通常性价比最高,再低就明显糊、还可能出现块状伪影。建议对普通照片 75~85,对截图/文字图(线条锐利)别压太低否则字糊,这类图 PNG 或无损更合适。不同图类型最优 quality 不同,按内容试。
第二,尺寸也要限。手机原图常 4000px 宽,展示最多 1280~1920 就够了,thumbnail 先把尺寸压下来,质量压缩才有效——只降 quality 不降尺寸,体积改善有限(大图细节多)。尺寸 + 质量双压,存储和流量都省。
第三,格式选择。照片类 JPEG 体积小;截图/带文字/需透明用 PNG(但 PNG 是无损、体积大)。如果业务允许,把 PNG 转 JPEG 能大幅瘦身(代价丢透明)。转换时记得 convert("RGB") 防 RGBA 报错,前面提过。
第四,大图内存。函数内存有限,几十 MB 的原图读进内存处理要注意函数内存配置,否则 OOM 失败。极端大图考虑先限制上传尺寸,或函数侧做流式/分块处理。
六、几个工程注意点
第一,循环触发三防护。分桶 + 触发器只监听源前缀 + 代码内前缀判断,三层防护最稳(见第四节)。这是这类 OBS 触发函数的头号事故源,务必上齐。
第二,压缩是破坏性的。有损压缩不可逆,原图如果还要保留(如用户想看大图),别覆盖源桶原图——源桶留原图、compressed 桶存压缩版,列表用压缩、详情用原图,和第 25 篇缩略图思路一致。
第三,格式一致性。压缩后统一 JPEG,前端展示就按 JPEG 处理;若某些图必须 PNG,函数要按原格式分支处理,别一刀切转 JPEG 把透明图弄黑底。
第四,触发器只勾上传事件。PutObject/PostObject 勾上,Delete 别勾,否则删原图又触发一次无意义压缩。
第五,成本。每次上传触发一次函数 + 一次 OBS 读写,量大自然有费用,但比常驻压缩服务按调用计费更省。函数内存配到刚好够处理最大图,别盲目给大(内存越大单价越高)。
七、小结
用 FunctionGraph + OBS 触发器做"上传即压缩":原图落地触发函数、Pillow 压尺寸+质量、写回压缩桶。压缩核心 thumbnail 限尺寸 + quality 控体积 + convert("RGB") 防 RGBA 存 JPEG 报错。防循环三防护(分桶/触发器只监听源前缀/代码内前缀判断)是头号重点;原图保留、压缩版写另桶,列表用小的、详情用原图。quality 75~85 性价比高、大图先限尺寸再压。函数依赖打包与触发器配置以官方文档为准。
- 点赞
- 收藏
- 关注作者
评论(0)