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

前言

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

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

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

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

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

创建桶之后注意需要修改访问权限,否则访问不到。

步骤一:配置cloudfront(cdn)

步骤二: 创建lmabda edge函数

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

js 复制代码
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版本的。以下是代码中使用到的两个依赖。

shell 复制代码
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

相关推荐
西猫雷婶1 小时前
STAR-CCM+|雷诺数回顾
云计算
坐吃山猪2 小时前
SpringBoot01-配置文件
java·开发语言
我叫汪枫3 小时前
《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
java·开发语言·nio
yaoxtao3 小时前
java.nio.file.InvalidPathException异常
java·linux·ubuntu
Swift社区4 小时前
从 JDK 1.8 切换到 JDK 21 时遇到 NoProviderFoundException 该如何解决?
java·开发语言
DKPT5 小时前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
phltxy5 小时前
JVM——Java虚拟机学习
java·jvm·学习
seabirdssss7 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续7 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升