用 Docker + Squoosh 打造图片压缩 API 服务

使用 Squoosh 的 Node.js 组件构建轻量图片压缩 API 服务,轻松集成到任意项目中,支持 JPG、PNG、WebP 等多种格式,压缩率高达 80%。

现在拍照设备拍的照片那是真的越来越大,网站如果涉及到很多图片,不去压缩的话存储空间和带宽是真吃不消。

于是就开始找解决办法,怎么使用程序压缩图片。试过很多方法,比如Pillow、OpenCV、Thumbnailator等,没有一款特别满意的,要么压缩后失真严重,要么压缩率不理想。

市面上在线的图片压缩工具压缩效果普遍不错,比如 TinyPNG、Squoosh、Optimizilla、Compress JPEG 等。我常用的是 TinyPNG,压缩率高、画质清晰,还提供 API 接口可以集成到自己的应用中。但问题是它收费太高,免费额度只有500张/月。

有一次无意间发现 Squoosh 有开源的 Node.js 组件,于是就动手写了一个 Node.js 服务,提供一个通用的图片压缩 API 给其他服务调用。

项目介绍

这个服务使用 Node.jsSquoosh CLI 构建,并通过 Docker 容器运行,方便快速部署到任意环境。

API 支持多种常见图片格式,包括JPG、PNG、WebP等。大部分图片压缩率能达到 80% 以上,而且几乎无损。

项目结构

├── Dockerfile
├── package.json
├── app.ts
└── src
    ├── config.ts
    ├── routers.ts
    └── imagecompress.ts

核心依赖

  • express- 提供 HTTP 服务
  • @squoosh/lib- 图片压缩核心组件
  • multer- 文件上传处理

Dockerfile 示例

FROM node:16.20.0-bullseye-slim

# 执行命令,创建文件夹
RUN mkdir -p /home/compress

# 设置工作目录
WORKDIR /home/compress

COPY . /home/compress

RUN npm install && \
    npm run build && \
    rm -rf /home/compress/src && \
    rm -rf /home/compress/*.ts && \
    rm -rf /home/compress/*.json && \
    rm -rf /home/compress/Dockerfile

# 配置环境变量
ENV HOST 0.0.0.0
ENV PORT 8866

# 容器对外暴露的端口号
EXPOSE 8866

#CMD ["npm", "run", "dev"]
CMD ["node", "/home/compress/dist/app.js"]

imagecompress.ts示例

import {  ImagePool, encoders, preprocessors }  from "@squoosh/lib";
import { cpus } from "os";
import { CompressOptions } from "@/types/config";

const imagePool = new ImagePool(cpus().length);


export async function compress(buffer:Buffer, ext: string, options: CompressOptions): Promise<Buffer> {
    let startTime = new Date().getTime();
    console.log("Start compressing pictures...", options);
    let image = imagePool.ingestImage(buffer);

    let preprocessOptions: any = {
        
    };

    let targetWidth = options.width && options.width > 0 ? options.width : undefined;
    let targetHeight = options.height && options.height > 0 ? options.height : undefined;

    if (targetWidth || targetHeight) {
        preprocessOptions.resize = {
            width: targetWidth,
            height: targetHeight
        }
    }

    let encodeOptions: any = {
        
    };

    await image.preprocess(preprocessOptions);
    await image.encode(encodeOptions);

    let targetFormat = options.target ? options.target.toLocaleLowerCase() : ext ? ext.replace(".", "").toLocaleLowerCase() : undefined;
    let result;

    if (targetFormat === "png") {
        encodeOptions.oxipng = {
            level: options.quality ? options.quality : 75
        };
        await image.encode(encodeOptions);
        result = await image.encodedWith.oxipng;
    } else if (targetFormat === "jxl") {
        encodeOptions.jxl = {
            quality: options.quality ? options.quality : 75
        };
        await image.encode(encodeOptions);
        result = await image.encodedWith.jxl;
    } else if (targetFormat === "avif") {
        await image.encode(encodeOptions);
        result = await image.encodedWith.avif;
    } else if (targetFormat === "webp") {
        encodeOptions.webp = {
            quality: options.quality ? options.quality : 75
        };
        await image.encode(encodeOptions);
        result = await image.encodedWith.webp;
    } else {
        encodeOptions.mozjpeg = {
            quality: options.quality ? options.quality : 75
        };
        await image.encode(encodeOptions);
        result = await image.encodedWith.mozjpeg;
    }
    
    
    if (!result) {
        return Buffer.alloc(0);
    }
    //const {extension, binary } = await image.encodedWith.mozjpeg;
    let array = new Uint8Array(result.binary.buffer);
    let buf: Buffer = Buffer.alloc(array.length);

    for (let nIndex = 0; nIndex < buf.length; nIndex++) {
        buf[nIndex] = array[nIndex];
    }
    
    console.log("Image compression completed! time consuming: " + (new Date().getTime() - startTime) + " ms.");
    console.log("Original image size: " + buffer.length + " Bytes, Compressed image size: " + buf.length + " Bytes, Compression ratio: " + Math.round(((buffer.length - buf.length) * 100 / buffer.length) * 100) / 100 + "%.");
    
    return buf;
}

routers.ts对外接口示例

router.post('/compress', upload.single('file'), function(req: Request, res: Response) {
    if (!req.file) {
        return res.status(400).send("No file uploaded!");
    } else if (req.file.size > 10485760) {
        return res.status(400).send("The maximum image size cannot exceed 10M!");
    }
    
    let options: CompressOptions = {
        quality: req.body.quality ? parseInt(req.body.quality) : undefined,
        width: req.body.width ? parseInt(req.body.width) : undefined,
        height: req.body.height ? parseInt(req.body.height) : undefined,
        target: req.body.target,
        extra: req.body.options
    };

    if (req.body.ct && req.body.ct === "attachment") {
        //download attachment
        res.setHeader("Content-Type", "application/octet-stream; charset=utf-8");
        res.setHeader("Content-Disposition", "attachment; filename=" + encodeURIComponent(req.file.originalname));
    } else {
        //Picture Preview
        //res.setHeader("Content-Type", req.file?.mimetype);
    }
    
    compress(req.file.buffer, path.extname(req.file.originalname).toLocaleLowerCase(), options).then((buffer: Buffer) => {
        res.end(buffer);
    });
});

使用方式

  1. 构建镜像:

    docker build -t imagecompress .
  2. 启动服务:

    docker run -d -p 8866:8866 imagecompress
  3. 上传并压缩图片:

    curl --location 'http://localhost:8867/compress' \
    --form 'file=@"/C:/Users/luoxudong/Pictures/1.png"' \
    --form 'quality="75"' \
    --form 'width="1000"' \
    --form 'ct="attachment"' \
    --form 'target="jpg"'
    

参数说明:
file: (必传) 文件内容,文件的key值为”file”。
quality: (选填) 图片压缩质量,不传时默认为75。
width: (选填) 设置压缩后输出图片的宽度,高度不设置时按等比例压缩。
height: (选填) 设置压缩后输出图片的高度,宽度不设置时按等比例压缩。
ct: (选填) 设置输出图片的Content-Type,“attachment”为以附件方式输出,其他为以预览图片方式输出。
target: (选填) 压缩后的图片格式,默认跟附件后缀一致,支持的值:”jpg” | “png” | “webp” | “avif” | “jxl。

压缩效果

实际测试中,使用 mozjpegWebP 编码方式时,图片压缩率普遍在 70%-85% 之间,肉眼几乎看不出差别。docker镜像已发布到dockerhub上,欢迎大家体验!

总结

相比第三方收费服务,自建的 Docker + Node.js + Squoosh 压缩服务更灵活、更可控,能轻松集成到自己的网站、后台管理或内容处理系统中。

后续可以进一步优化支持异步任务、批量压缩、分布式处理等功能,让图片资源管理更加高效。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注