使用 ImageMagick 和 libvips 进行服务器端批量图片压缩

发表于 2026-01-07 01:38 3459 字 18 min read

cos avatar

cos

FE / ACG / 手工 / 深色模式强迫症 / INFP / 兴趣广泛养两只猫的老宅女 / remote

暂无目录
文章介绍了在无GUI环境下批量压缩历史图片的实践过程,最终选择ImageMagick处理小图、libvips处理大图的组合方案,并强调了备份的重要性。通过mogrify和vipsthumbnail命令实现高效批量压缩,同时提供了优化参数(如-strip、-sampling-factor、-quality设置及文件大小限制)以达到最佳压缩效果。

前言

最近需要处理别人的服务器上大量历史图片的压缩问题。因为历史遗留原因,public/uploads 下累积了不少高分辨率的全景图和常规照片,占用存储空间越来越大。虽然之前在前端生产环境中一直使用 sharp-cli@napi-rs/image 等进行图片处理,但这次面对的是已存储的历史图片批量压缩场景,而且这次提供的环境是堡垒机,只能通过网页终端进行操作,没有 GUI,需要更灵活的命令行工具。

这篇文章记录了完整过程,希望对有类似需求的朋友有所帮助。

为什么不用 sharp-cli?

sharp-cli@napi-rs/image 一般是我在生产环境中的首选,性能优异且 API 友好。但这次需求是批量处理已存储的大量图片,而 sharp-cli 更适合集成到构建流程或 Node.js 脚本中。对于这种一次性批量操作,传统的 ImageMagick/libvips 命令行工具更加直接。

工具对比

在压缩之前我看过了这些工具:

工具优势劣势适用场景
ImageMagick功能全面,文档丰富大图处理内存占用高通用图片处理
libvips流式处理,内存效率极高功能相对聚焦超大图片/批量处理
sharp-cli性能优异,API 友好需要 Node.js 环境构建流程集成
Squoosh CLI支持现代格式(WebP/AVIF)已停止维护❌ 不推荐生产使用
guetzli/mozjp极致压缩(JPEG)处理速度极慢追求极限压缩率场景

最终选择:ImageMagick + libvips 组合方案

  • 一开始使用 ImageMagick 处理 10MB 以下的常规图片,很快速很方便,但发现超大图片(100MB+)会爆内存。
  • libvips 是 sharp 底层使用的库,处理超大全景图(100MB+)绰绰有余。
infographic compare-hierarchy-left-right-circle-node-pill-badge
data
  title 工具选择策略
  items
    - label ImageMagick
      children
        - label 常规图片
          desc <10MB 的图片
        - label 功能全面
          desc 支持各种格式转换
        - label 语法简单
          desc mogrify 一行命令
    - label libvips
      children
        - label 超大图片
          desc 100MB+ 全景图
        - label 内存效率
          desc 流式处理不爆内存
        - label sharp底层
          desc 生产级性能保证

开始前准备

infographic list-row-simple-horizontal-arrow
data
  title 操作流程
  items
    - label 检查容量
      desc 查看磁盘剩余空间
    - label 统计大小
      desc 评估压缩前文件大小
    - label 备份数据
      desc 防止误操作
    - label 执行压缩
      desc 使用合适工具

最重要的两点当然是:

  1. 查看原来的所有图片大小
  2. 备份你要压缩的图片(因为 mogrify 会在原图的基础上直接进行压缩,所以备份很重要)
# du(disk usage)
# -sh: 只显示每个参数的总大小,以人类可读的格式显示(K, M, G)
# sort -hr: 按大小倒序排列
du -sh * | sort -hr
# 只列出 17 开头的
du -sh 17* | sort -hr

输出如下:

root@3d:/myapp/storage/uploads/panorama/backup# du -sh * | sort -hr
147M    1766019200667-1c2d8x.jpg
130M    1766019053272-aylt0b.jpg
120M    1766019107903-jl2i2s.jpg
63M     1765977415640-hu7yap.jpg
……

然后备份,备份前记得 df -h 查看磁盘剩余可用空间。

df -h
# 可用空间足够,则进行备份
tar -cvf uploads_backup_$(date +%Y%m%d).tar uploads/
# 或者比如说我要备份 17 开头的
tar -czvf backup_17_$(date +%Y%m%d).tar.gz 17*
# 备份所有 17 开头且大于 10M 的文件
tar -czvf backup_17_large_$(date +%Y%m%d).tar.gz $(find . -name "17*" -size +10M)

会把 upload 目录备份为 uploads_backup_20251223.tar 文件

工具安装

然后就到了开始实行压缩的环节

ImageMagick

# Debian/Ubuntu
sudo apt update && sudo apt install imagemagick -y

# macOS
brew install imagemagick

# 验证
magick -version

需要注意的是新版 ImageMagick 7+ 推荐使用 magick 命令,但是服务器上比较老所以我用的旧语法。

libvips

# Debian/Ubuntu
sudo apt install libvips-tools -y

# macOS
brew install vips

# 验证
vipsthumbnail --version

踩坑记录

超大图片内存溢出

处理 150MB 的全景图时遭遇 cache resources exhausted 错误:

# 假设以 change 开头的是我们要批量压缩的图片
# 原图可能有 20000x 以上,可以 resize 到 9000x 以下
mogrify -quality 75 -resize "9000x>" ./change*.jpg

根本原因:

ImageMagick 采用全图加载到内存的处理方式:

对于 150MB 的航拍全景图 JPEG 文件,其实际分辨率可达 23000×11000 左右。解压后的像素数据大小为:

20000 × 10000 × 3 字节 = 600MB(RGB 格式)

ImageMagick 在处理过程中还需要:

  • 创建多个中间缓冲区
  • 存储变换矩阵
  • 保存临时结果

因此实际内存占用可达 900MB-2GB,超出了默认资源限制。(我不确定这里的描述是否准确,但反正那个小水管服务器上是跑不了的)

ImageMagick 处理超大图很吃力,所以我请出了 libvips

# 1. 先看看有哪些文件
find . -maxdepth 1 -name "17*.jpg" -size +10M -exec ls -lh {} \;

# 2. 备份(安全起见)
tar -czvf backup_17_10M_$(date +%Y%m%d).tar.gz 17*.jpg

# 3. 执行压缩
for f in 17*.jpg; do
  size=$(stat -c%s "$f" 2>/dev/null)
  if [ $size -gt 10485760 ]; then
    echo "Compressing: $f ($(du -h "$f" | cut -f1))"
    vipsthumbnail "$f" -s 9000 -o "temp_[Q=75].jpg"
    if [ -f "temp_.jpg" ]; then
      mv "temp_.jpg" "$f"
      echo "Done: $f → $(du -h "$f" | cut -f1)"
    fi
  fi
done

# 4. 检查结果
du -sh 17*.jpg | sort -hr

注意这里 temp_[Q=75].jpg 中的 [Q=75] 会被替换为空,这是 vipsthumbnail 的文件名替换规则。

在 vipsthumbnail 命令中,-o 参数支持特殊的占位符替换。而 [Q=75] 是一个保存选项占位符,用于指定 JPEG 质量

在最终生成的文件名中,这部分会被移除(替换为空)

大图片压缩圆满完成。

分目录 mogrify 批量压缩

注意 mogrify 会在原图的基础上直接进行压缩,所以备份很重要

较小的图片使用 ImageMagick 的 mogrify 进行就地批量修改:

# 先备份
cp -r . ../backup_before_mogrify/
# 就地修改(mogrify)
mogrify -quality 75 -resize "4000x>" input.jpg
# 或输出到新文件(convert)
convert input.jpg -quality 75 -resize "4000x>" output.jpg

其他常用的 resize 选项:

"4000x>"     # 只缩小,宽度限制4000px
"4000x4000>" # 只缩小,宽高都不超过4000px
"50%"        # 缩小到50%
"4000x3000!" # 强制缩放到指定尺寸(可能变形)

有很多的子目录

root@3d:/myapp/storage/uploads/panorama# du -sh * | sort -hr
131M    cmjpqjihk002mlm01qyyk3vqc
131M    cmjpp9pfk001ulm01dr2aina9
129M    cmjxtmsfs00eilm01iso8kaq2
128M    cmja74wwh00fcl701ie777fnj
127M    cmja751nb00fel701u4oqmgfk
126M    cmk0yw2zx004nlp01i7r22uoe

里面是这种格式的相对小的图片。

root@3d:/myapp/storage/uploads/panorama/cmjpqjihk002mlm01qyyk3vqc# du -sh * | sort -hr
26M     cmjpqjihk002mlm01qyyk3vqc.right.jpg
26M     cmjpqjihk002mlm01qyyk3vqc.left.jpg
25M     cmjpqjihk002mlm01qyyk3vqc.back.jpg
24M     cmjpqjihk002mlm01qyyk3vqc.front.jpg
18M     cmjpqjihk002mlm01qyyk3vqc.bottom.jpg
13M     cmjpqjihk002mlm01qyyk3vqc.top.jpg

那我就直接用 mogrify -quality 75 -resize "4000x>" ./public/uploads/**/*.jpg 了,快一些。

我需要压缩大于 20M 的目录,并且批量先 cd 进目录,处理完再 cd ..,过程中我还想看进度:

先备份大于 20M 的目录

backup_file="../panorama_backup_$(date +%Y%m%d_%H%M%S).tar.gz"
dirs_to_backup=""

for dir in */; do
  size=$(du -sm "$dir" | cut -f1)
  if [ $size -gt 20 ]; then
    echo "加入备份: $dir ($size MB)"
    dirs_to_backup="$dirs_to_backup $dir"
  fi
done

tar -czf "$backup_file" $dirs_to_backup
echo "备份完成: $backup_file"

备忘录

  • du -sm 中的 -m 参数指定了单位是 MB(megabytes)
  • -s 是 summarize(汇总)
  • -m 是 megabytes(兆字节)
  • cut -f1 提取第一列(数字部分)
  • tar -czf 创建一个用 gzip 压缩的 tar 归档文件,名字叫 backup.tar.gz
  • -c = create 创建新的归档文件
  • -z = gzip 使用 gzip 压缩
  • -f = file 指定文件

开始压缩,使用 mogrify -quality 75 -resize "4000x>" "$img"(ImageMagick 7+ 推荐使用 magick mogrify 代替旧版的 mogrify

使用 ImageMagick 压缩和调整图片

  • mogrify 直接修改原图片(不创建副本)
  • -quality 75 设置 JPEG 质量为 75%
  • -resize "4000x>" 如果宽度超过 4000 像素就缩小,保持宽高比,> 表示只缩小不放大

以下命令的大意是:

  • 遍历所有以 cmj 开头的子目录,检查每个目录的大小
  • 筛选大于 20 MB 的目录进行处理(跳过已经压缩过的小目录)
  • 对符合条件的目录:进入目录 -> 统计其中的 JPG 文件数量 -> 逐个处理每张 JPG 图片 -> 降低质量到 75%,如果宽度超过 4000 像素则缩小到 4000 像素(保持宽高比)
  • 显示实时进度(如 [3/10] photo.jpg)
  • 处理完成后返回上级目录,继续处理下一个目录
for dir in cmj*/; do
  size=$(du -sm "$dir" | cut -f1)
  if [ $size -gt 20 ]; then
    echo "处理: $dir ($size MB)"
    cd "$dir"
    total=$(ls *.jpg 2>/dev/null | wc -l)
    count=0
    for img in *.jpg; do
      ((count++))
      echo "  [$count/$total] $img"
      mogrify -quality 75 -resize "4000x>" "$img"
    done
    cd ..
    echo "完成: $dir"
  fi
done

备忘录

  • wc -l 中的 -l 是 lines(行数),统计行数
  • ((count++)) 是 bash 的算术运算语法,让变量自增 1
  • mogrify 是 ImageMagick 工具,直接修改原文件而不创建副本
  • -quality 75 设置 JPEG 压缩质量为 75%(0-100,数值越高质量越好文件越大)
  • -resize "4000x>" 中的 > 表示”只缩小不放大”,如果图片宽度 ≤4000 则不处理

效果拔群。

写在最后

为什么不直接用 sharp-cli?

sharp 非常优秀,我在生产环境的图片上传流程中一直使用。但对于已存储的历史图片批量压缩,传统命令行工具更灵活:

  • 不需要写 Node.js 脚本
  • 直接用 shell 管道/find 组合
  • libvips 本身就是 sharp 的底层引擎
  • 下次再用直接改改就可以

如何达到最优的压缩效果?

更细致的压缩质量的话,可以看这篇 Stack Overflow 帖子,很具体地讨论了使用 ImageMagick 压缩 JPEG 图片文件以实现最小文件大小但又不损失过多质量的问题。

以下是 AI 辅助总结的该帖子的要点总结

问题

原始提问者(Javis Perez)遇到的问题是:

  1. 压缩效果不佳: 使用 ImageMagick 压缩 JPEG 文件时,无法获得显著的文件大小差异。
  2. 输出文件反而更大: 在默认情况下,输出的图片文件大小甚至比输入文件更大。
  3. 尝试后仍不理想: 即使使用了 +profile "*"-quality 70 选项,输出文件(264KB)仍然接近或略大于输入文件(255KB)。
  4. 明确的目标: 希望能将图片压缩到至少 150KB,并寻求实现这一目标的 ImageMagick 选项。

解决方式

帖子中提供了多种解决方案和参数组合,可以归纳为以下几点:

  1. 核心参数组合 (最受推荐和高赞的方案):

    • Command: convert -strip -interlace Plane -gaussian-blur 0.05 -quality 85% source.jpg result.jpg
    • 参数解释:
      • -quality 85%: 设置 JPEG 压缩质量为 85%(可根据需求调整,通常在 60-85 之间,数字越低文件越小,但质量损失越大)。
      • -strip: 移除图片中的所有元数据(如 EXIF 信息、评论等),这是无损缩小文件大小的有效方法。
      • -interlace Plane (或 -interlace JPEG): 创建渐进式 JPEG,虽然不直接减小文件大小,但能改善图片在网络上的加载体验。
      • -gaussian-blur 0.05: 应用一个非常小的(如 0.05)高斯模糊。这个参数有争议,一些用户认为它能平滑图像中的噪点,从而提高压缩率,显著减小文件大小;另一些用户则认为它会造成图像模糊,不如直接降低-quality
  2. Google PageSpeed Insights 推荐的优化方案:

    • Command: convert image.jpg -sampling-factor 4:2:0 -strip -quality 85 -interlace JPEG -colorspace RGB image_converted.jpg
    • 参数解释:
      • -sampling-factor 4:2:0: 这是色彩子采样,将色度通道的采样率减半,同时不影响亮度通道。人眼对亮度变化比色度变化更敏感,因此这种方法能在不明显影响视觉质量的情况下大幅度减小文件大小。
      • -strip-quality 85-interlace JPEG: 同上。
      • -colorspace RGB: 确保图片以正确的 RGB 色彩空间处理。
    • 补充选项: -define jpeg:dct-method=float: 使用更精确的浮点离散余弦变换(DCT)而不是默认的快速整数版本,可以在不增加文件大小的情况下略微提高转换的保真度。
  3. 直接指定目标文件大小:

    • Command: convert image.jpg -define jpeg:extent=150kb result.jpg
    • 参数解释:
      • -define jpeg:extent={size}: 从 ImageMagick v6.5.8-2 开始,可以直接指定 JPEG 输出文件的最大大小(例如 “150kb”)。ImageMagick 会自动调整质量以达到这个目标大小。这是解决原始问题中”压缩到 150kb”最直接的方法,但会带来质量损失。
  4. 图像尺寸调整(最显著的减小文件大小的方法):

    • Command (示例): mogrify -quality "97%" -resize 2048x2048 -filter Lanczos -interlace Plane -gaussian-blur 0.05
    • 参数解释:
      • -resize WxH-adaptive-resize X%: 调整图像的尺寸。这是减小文件大小最有效的方法之一,尤其是对于高分辨率图片。
      • -filter Lanczos: 一种高质量的缩放滤镜(通常是默认值)。
    • 注意: 减小图片尺寸会改变图片的实际像素数据。
  5. 其他考量及建议:

    • 批处理: 对于多个文件,可以使用mogrify命令替代convert。例如:mogrify -strip -interlace Plane -gaussian-blur 0.05 -quality 85% *.jpg注意: mogrify会覆盖原文件,操作前务必备份。
    • 质量与模糊的权衡: 帖子中多次提及 -gaussian-blur 的争议。许多用户认为简单地降低 -quality 即可达到目的,或者结合 -sampling-factor 4:2:0 更有效,避免引入不必要的模糊。对于大尺寸图片,微小的模糊可能不易察觉;对于小图片,则效果可能不佳。
    • 外部工具: 推荐使用如 ImageOptim、pngquant 等外部工具进行进一步的无损或有损优化。
    • 重新压缩的副作用: 有评论指出,对已压缩的 JPEG 进行再压缩,即使文件大小增大,也会导致图像质量下降。

Refs

本文随时修订中,有错漏可直接评论

喜欢的话,留下你的评论吧~