aws S3利用lambda edge实现图片缩放、质量转换等常规图片处理功能

发布于:2025-03-30 ⋅ 阅读:(20) ⋅ 点赞:(0)

前言

  • 与阿里的oss不同的是S3不支持通过url参数实现这种类似黑盒的图片处理,而是提供了一种特殊的lambda函数,我虽然不清楚为什么叫做lambda函数,但其本质就是一个拦截器。下面就演示一下利用lambda函数实现图片缩放和质量转换。

  • cloudfront是什么?这个是一个缓存服务,就是经常听说的cdn

  • 有四个阶段可以做图片处理,下文选择的是在origin request事件中做图片处理。
    在这里插入图片描述

有几个前置步骤,这里只列出,不做细致演示。

  • aws账号注册,注册后需要绑定一张信用卡才能使用。新注册用户有12个月的免费资源使用。从绑定银行卡之日起计算。
  • 创建存储桶,这个和阿里OSS一样。

创建桶之后注意需要修改访问权限,否则访问不到。
在这里插入图片描述

步骤一:配置cloudfront(cdn)

  • 创建地址:https://us-east-1.console.aws.amazon.com/cloudfront/v4/home?region=us-east-1#/distributions
  • 注意cloudfront必须配置在佛吉尼亚北部
    在这里插入图片描述
    在这里插入图片描述
    这里需要注意一下, 启用了防火墙之后价格是按照请求另外计算的,防火墙不在免费资源范围内
    在这里插入图片描述
    创建完成之后,回到首页
    在这里插入图片描述
    点进去之后,这里就可以看到域名了。尝试使用该域名访问:https://图片中的域名/resized-boke.jpg
    在这里插入图片描述
    现在cloudfront就基本配置完成了。

步骤二: 创建lmabda edge函数

  • 创建地址:https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/discover
    在这里插入图片描述
    创建函数时注意选择蓝图(这个一看就是机器翻译的,其实就是模板的意思)。执行角色这个不管选择哪个,后面都要去配置一下角色权限(https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/roles),防止出现执行函数异常的问题,或者日志不打印的问题。
    在这里插入图片描述
    注意一路向下部署完成后,此时版本1已经生效,回到函数页面点击更新函数,编写代码。
    在这里插入图片描述

步骤三:编写lambda函数代码:

import {GetObjectCommand, S3Client} from '@aws-sdk/client-s3';
import sharp from 'sharp';
import {Buffer} from 'buffer';
import {Readable} from 'stream';
import {parse} from 'querystring';

const bucketName = 'chengzhi-test-resized';

/**
 * 多个处理参数使用&符号分隔: /test.png@resize=50x50&width=50&height=50&proportion=50&quality=50
 * 图片处理参数类型:
 * - 按照像素缩放:    @resize=50x50
 * - 按照宽度等比缩放: @width=20
 * - 按照高度等比缩放: @height=200
 * - 按照比例缩放:    @proportion=99
 * - 质量转换:       @quality=50
 * @param event
 * @param context
 * @returns
 */
export const handler = async (event, context) => {

    const cf = event.Records[0].cf;
    const request = cf.request;

    const params = getParams(request.uri);
    if (params === undefined) {
        console.log("未携带处理参数,不进行处理!");
    } else {
        let response = '';
        const image = params.image;
        const objectKey = `${image}`;
        const trigger_point = cf.response ? 'RESPONSE' : 'REQUEST';
        if (trigger_point === 'REQUEST') {
            let buffer = await getObjectBuffer(bucketName, objectKey);
            if (buffer === undefined) {
                return request;
            }

            if (params.resize) {
                buffer = await resizeImage(buffer, params.resize);
            }

            if (params.width) {
                buffer = await widthImage(buffer, params.width);
            }

            if (params.height) {
                buffer = await heightImage(buffer, params.height);
            }

            if (params.proportion) {
                buffer = await proportionImage(buffer, params.proportion);
            }

            if (params.quality) {
                buffer = await qualityImage(buffer, image.split('.')[1],params.quality);
            }
            return generateResponse(response, buffer, trigger_point);
        }
    }
    return request;
};

async function getObjectBuffer(bucket_name, objectKey) {

    const region = "eu-north-1";
    const s3 = new S3Client({
        region: region
    });
    try {
        const params = {
            Bucket: bucket_name,
            Key: objectKey
        };
        var response = await s3.send(new GetObjectCommand(params));
        var stream = response.Body;
        if (stream instanceof Readable) {
            var content_buffer = Buffer.concat(await stream.toArray());
            return content_buffer;
        } else {
            throw new Error('Unknown object stream type');
        }
    } catch (error) {
        console.log(error);
        return;
    }
}

async function heightImage(content_buffer, height) {
    try {
        let pipeline = sharp(content_buffer);
        height = Number(height);
        let resizeConfig = {
            height: height
        };
        pipeline = pipeline.resize(resizeConfig);
        return await pipeline.toBuffer();
    } catch (error) {
        console.log(error);
        return;
    }
}
async function widthImage(content_buffer, width) {
    try {
        let pipeline = sharp(content_buffer);
        width = Number(width);
        let resizeConfig = {
            width: width
        };
        pipeline = pipeline.resize(resizeConfig);
        return await pipeline.toBuffer();
    } catch (error) {
        console.log(error);
    }
}
/**
 * 等比例缩放图片
 * @param content_buffer
 * @param proportion       百分比(0-100)
 * @returns {Promise<*>}
 */
async function proportionImage(content_buffer, proportion) {
    try {
        let pipeline = sharp(content_buffer);
        const metadata = await pipeline.metadata();
        proportion = Number(proportion);
        const percentage = proportion / 100;
        let resizeConfig = {
            width: Math.round(metadata.width * percentage)
        };
        pipeline = pipeline.resize(resizeConfig);
        return await pipeline.toBuffer();
    } catch (error) {
        console.log(error);
    }
}
/**
 * 裁剪图片
 * @param content_buffer
 * @param size            例如:50x40
 * @returns {Promise<*>}
 */
async function resizeImage(content_buffer, size) {
    try {
        const [width, height] = size.split('x').map(Number);
        var output_buffer = await sharp(content_buffer).resize(width,height).toBuffer();
    } catch (error) {
        console.log(error);
        return;
    }
    return output_buffer;
}

/**
 * 图片质量转换
 * @param content_buffer
 * @param format            图片格式
 * @param quality           目标质量(0-100)
 * @returns {Promise<*>}
 */
async function qualityImage(content_buffer, format, quality) {
    try {
        // 传入的quality为字符串
        quality = Number(quality);
        console.log('quality:', quality)
        console.log('format:', format)
        let pipeline = sharp(content_buffer);
        // 根据格式设置质量
        switch (format.toLowerCase()) {
            case 'jpeg':
            case 'jpg':
                pipeline = pipeline.jpeg({
                    quality: quality,
                    mozjpeg: true  // 使用 mozjpeg 优化
                });
                break;
            case 'png':
                pipeline = pipeline.png({
                    quality: quality,
                    compressionLevel: 9  // PNG 压缩级别 0-9
                });
                break;
            case 'webp':
                pipeline = pipeline.webp({
                    quality: quality,
                    lossless: false  // 有损压缩
                });
                break;
            case 'avif':
                pipeline = pipeline.avif({
                    quality: quality,
                    lossless: false
                });
                break;
        }
        console.log('质量转换完成!')
        var outputBuffer = await pipeline.toBuffer();
        console.log('流转换完成!')
    } catch (error) {
        console.log(error);
        return;
    }
    return outputBuffer;
}
function generateResponse(response, buffer, trigger_point) {
    if (trigger_point === 'REQUEST') {
        response = {
            status: '',
            statusDescription: '',
            headers: {
                'cache-control': [{
                    key: 'Cache-Control',
                    value: 'max-age=100'
                }],
                'content-type': [{
                    key: 'Content-Type',
                    value: 'image/png'
                }],
                'content-encoding': [{
                    key: 'Content-Encoding',
                    value: 'base64'
                }]
            },
            body: '',
            bodyEncoding: 'base64'
        };
    }

    response.status = '200';
    response.body = buffer.toString('base64');
    response.bodyEncoding = 'base64';
    response.headers['content-type'] = [{
        key: 'Content-Type',
        value: 'image/png'
    }];
    response.headers['content-encoding'] = [{
        key: 'Content-Encoding',
        value: 'base64'
    }];

    response.statusDescription = trigger_point === 'REQUEST'
        ? 'Generated by CloudFront Original Request Function'
        : 'Generated by CloudFront Original Response Function';

    return response;
}

function getParams(image) {
    image  = image.startsWith('/') ? image.slice(1) : image
    if (!image.includes('@')) {
        console.log("不包含@符号,不需要进行处理!");
        return;
    }
    var strings = image.split('@');
    if (strings.length < 2) {
        console.log("@符号位置不正确,不进行处理");
        return;
    }
    var result = parse(strings[1]);
    var picture = strings[0];
    if (picture === undefined || !picture.match(/\.(jpg|jpeg|png|gif|webp)$/i)) {
        console.log("非图片类,不进行处理");
        return;
    }
    if (result !== undefined) {
        result.image = picture;
    }

    console.log("图片处理参数:", JSON.stringify(result))
    return result;
}

函数编写完成后,需要将依赖打包,注意因为是要在aws的服务器上运行该代码,所以引入的依赖必须是linux版本的。以下是代码中使用到的两个依赖。

npm install --arch=x64 --platform=linux --target=16x sharp@0.32.6
npm install --arch=x64 --platform=linux --target=16x aws-sdk@2.1450.0 

依赖引入完成后,需要打包成zip包,包结构如图,如果zip包超过10M,上传会超时,需要使用S3上传,注意上传代码的S3也必须是弗吉尼亚北部。否则上传会失败。
在这里插入图片描述

代码上传完成后,一定要修改函数执行超时时间,否则函数运行1s后,就自动停止了。

在这里插入图片描述

步骤四:部署

代码完成后,点击添加触发器,选择cloudfront(注意如果函数不是在弗吉尼亚北部,这个是选择不到的),按照提示填写配置。注意我上面的代码cluodfront事件选择的是源请求
在这里插入图片描述

步骤五: 测试

https://域名/novel1.png@height=200
在这里插入图片描述
验证是否生成缓存
https://us-east-1.console.aws.amazon.com/cloudfront/v3/home?region=us-east-1#/popular_urls
在这里插入图片描述

查看日志

查看日志的时候需要注意右上角的地区,由于lambda函数是部署在cloudfront上的,而cloudfront是全球分布多个节点,一般本地访问都是优先连接附近的节点,因此日志也会出现在附近的地区节点上,比如,我在上海访问,那么日志会随机落到东京孟买。如果IAM角色权限未正确配置(https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/roles),不会打印日志。
在这里插入图片描述

答疑

  • 1、为什么要在url中使用@?
    答: 在测试过程中发现,函数中无法获取到queryString的参数,也就是说无法获取到url问号后面的参数部分,因此采用@然后在程序中做分割处理,这样一来可以解决获取参数的问题, 二来可以解决图片名称相同但尺寸不同的缓存问题。

  • 2、为什么要在源请求中做处理?
    答: 刚开始也考虑在源响应中做处理,但是发现源响应的response中并没有body,咨询了aws的售前说源响应是没有body的。
    在这里插入图片描述

  • 3、是否可以兼容ali oss图片处理参数?
    答:基于上述内容可以发现,lambda函数使用的是sharp库,这个库中支持的那大概率都能实现,我只是因为只需要用到这几个处理方式。

  • 4、为什么使用Node,不适用python?
    答: 刚开始尝试使用了python,但是打包上传到服务上时提示,有包冲突,谷歌了一下,说是需要用到aws的一个,个人觉比较复杂,就更换了node.

参考文档:

https://aws.amazon.com/cn/blogs/china/use-cloudfront-lambdaedge-for-transaction-processing/
https://docs.aws.amazon.com/zh_cn/lambda/latest/dg/getting-started.html#get-started-invoke-manually
https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html
https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.SimpleDistribution.html
https://docs.aws.amazon.com/zh_cn/lambda/latest/dg/with-s3-tutorial.html
https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/popular-objects-report.html


网站公告

今日签到

点亮在社区的每一天
去签到