在没有写这个功能之前,我压缩图片用得最多的是这个工具,相信大家也使用过,但这个工具有个不好的点,有时候无法满足我自己的业务需求,比如上传图片大小有限制,超过5M的就不让操作了,批量压缩数量只要多一点就被限制了,而且要收费,想要压缩的大小也无法指定;想着还是自己写一个吧

我要做的是免费能帮助大家的实用工具,我做的目的就是能自由选择大小压缩,无限制免费使用,支持批量;如果有喜欢的可以点击收藏一下,创作不容易:在线免费图片压缩-支持批量

前端后端实现方式:前端就是一些简单的样式编写,上面就是我自己随便编写的样式,想做的可以按照自己的思路去调整;后端处理想要做效果必须使用:(ImageMagick或者Ffmpeg),最终效果肯定是ImageMagick最好,Ffmpeg次之,其他的工具不推荐;在自己的服务器或者本地环境安装上这个软件,代码中直接使用命令调用;下面我就把后端代码开放出来,想要前后端的完整代码的,可以进入点透办公:https://office.zjdiante.com/developer 联系我
后端代码如下:controller层
java
/**
* 图片压缩接口(本机 ImageMagick {@code magick},见 {@link FfmpegImageCompressUtils})。按压缩比例 10%~70% 处理;多文件每次最多 10 张,返回 ZIP。
*
* @param file 单文件上传(与 files 二选一)
* @param files 多文件上传,最多 10 个;与 file 同时存在时以 files 为准
* @param compressPercent 压缩强度 10~70,不传默认 30
*/
@PostMapping("/compress")
public ResponseEntity<?> compressImage(
@RequestParam(value = "file", required = false) MultipartFile file,
@RequestParam(value = "files", required = false) MultipartFile[] files,
@RequestParam(value = "compressPercent", required = false) Integer compressPercent
) {
try {
List<MultipartFile> fileList = new ArrayList<>();
if (files != null && files.length > 0) {
for (MultipartFile f : files) {
if (f != null && !f.isEmpty()) {
fileList.add(f);
}
}
} else if (file != null && !file.isEmpty()) {
fileList.add(file);
}
if (fileList.isEmpty()) {
throw new ServiceException("上传的文件不能为空");
}
if (fileList.size() > 10) {
throw new ServiceException("每次最多上传10张图片");
}
final long maxTotalBytes = 50L * 1024 * 1024;
long totalBytes = 0L;
for (MultipartFile f : fileList) {
totalBytes += f.getSize();
}
if (totalBytes > maxTotalBytes) {
throw new ServiceException("文件过大,请分批处理");
}
int percent = compressPercent != null ? compressPercent : 30;
if (percent < 10 || percent > 70) {
throw new ServiceException("压缩比例需在10%~70%之间");
}
percent = FfmpegImageCompressUtils.normalizeCompressPercent(percent);
final long maxBytesPerFile = 20L * 1024 * 1024;
for (MultipartFile f : fileList) {
if (f.getSize() > maxBytesPerFile) {
throw new ServiceException("单张图片不能超过20MB");
}
String name = f.getOriginalFilename();
if (name == null || !isSupportedImageFormat(name)) {
throw new ServiceException("仅支持 PNG/JPG/JPEG/WebP/GIF/BMP 格式的图片");
}
}
try {
if (fileList.size() == 1) {
MultipartFile mf = fileList.get(0);
String originalFilename = mf.getOriginalFilename();
byte[] raw = IoUtil.readBytes(mf.getInputStream());
byte[] compressedBytes = FfmpegImageCompressUtils.compressBytes(
raw, originalFilename, null, percent);
HttpHeaders headers = buildCompressResponseHeaders(originalFilename, compressedBytes);
return new ResponseEntity<>(compressedBytes, headers, HttpStatus.OK);
}
ByteArrayOutputStream zipBuffer = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(zipBuffer)) {
int index = 0;
for (MultipartFile mf : fileList) {
String originalFilename = mf.getOriginalFilename();
byte[] raw = IoUtil.readBytes(mf.getInputStream());
byte[] compressedBytes = FfmpegImageCompressUtils.compressBytes(raw, originalFilename, null, percent);
String entryName = safeZipEntryName(originalFilename, index++, compressedBytes);
zos.putNextEntry(new ZipEntry(entryName));
zos.write(compressedBytes);
zos.closeEntry();
}
}
byte[] zipBytes = zipBuffer.toByteArray();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/zip"));
String zipName = "compressed_images_" + System.currentTimeMillis() + ".zip";
headers.setContentDisposition(
ContentDisposition.builder("attachment").filename(zipName, StandardCharsets.UTF_8).build());
return new ResponseEntity<>(zipBytes, headers, HttpStatus.OK);
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("图片压缩失败: {}", e.getMessage());
throw new ServiceException("服务正忙,请稍后再试!");
}
}
utils工具类
java
package com.ruoyi.basicTool.imageTool.utils;
import cn.hutool.core.io.IoUtil;
import org.apache.commons.lang3.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* 使用本机已安装的 ImageMagick({@code magick})进行图片压缩(不依赖第三方在线服务)。
* <p>可执行文件解析顺序:系统属性 {@code imagemagick.binary} → 环境变量 {@code IMAGEMAGICK_BINARY} → 默认 {@code magick}。</p>
*/
public final class FfmpegImageCompressUtils {
private FfmpegImageCompressUtils() {
}
public static InputStream compressBase(InputStream imageStream, String originalFilename) throws Exception {
if (imageStream == null) {
throw new IllegalArgumentException("图片流不能为空");
}
byte[] original = IoUtil.readBytes(imageStream);
if (original.length == 0) {
throw new Exception("图片流内容为空,无法压缩");
}
byte[] out = compressBytes(original, originalFilename, null, null);
return new ByteArrayInputStream(out);
}
public static InputStream compressToTargetSize(InputStream imageStream, int targetSizeKB, String originalFilename) throws Exception {
if (imageStream == null) {
throw new IllegalArgumentException("图片流不能为空");
}
int actualTarget = targetSizeKB <= 0 ? 10 : targetSizeKB;
byte[] original = IoUtil.readBytes(imageStream);
if (original.length == 0) {
throw new Exception("图片流内容为空,无法压缩");
}
byte[] out = compressBytes(original, originalFilename, actualTarget, null);
return new ByteArrayInputStream(out);
}
/**
* @param targetSizeKB 目标上限(KB),{@code null} 表示不按目标体积迭代
*/
public static byte[] compressBytes(byte[] inputBytes, String originalFilename, Integer targetSizeKB) throws Exception {
return compressBytes(inputBytes, originalFilename, targetSizeKB, null);
}
/**
* @param targetSizeKB 目标体积(KB),与开放平台按 KB 压缩一致;{@code null} 时不走该分支
* @param compressPercent 压缩强度 10--70(数值越大目标体积越小);与 targetSizeKB 互斥时优先 targetSizeKB。
*/
public static byte[] compressBytes(byte[] inputBytes, String originalFilename, Integer targetSizeKB, Integer compressPercent) throws Exception {
if (inputBytes == null || inputBytes.length == 0) {
throw new IllegalArgumentException("图片数据为空");
}
String fn = StringUtils.isNotBlank(originalFilename) ? originalFilename : "image.jpg";
String ext = suffixOf(fn);
Path tempIn = null;
Path tempOut = null;
try {
tempIn = Files.createTempFile("imc_in_", ext);
Files.write(tempIn, inputBytes);
if (targetSizeKB != null) {
long targetBytes = (long) targetSizeKB * 1024L;
return compressToTargetBytes(tempIn, ext, targetBytes);
}
if (compressPercent != null) {
int p = normalizeCompressPercent(compressPercent);
long targetBytes = computeTargetBytesForSlider(inputBytes.length, p);
return compressToTargetBytes(tempIn, ext, targetBytes);
}
String outExt = outputExtensionForBase(ext);
tempOut = Files.createTempFile("imc_out_", outExt);
runMagickBase(tempIn, tempOut, ext);
return Files.readAllBytes(tempOut);
} finally {
deleteQuietly(tempIn);
deleteQuietly(tempOut);
}
}
/**
* 将滑块 10~70 映射为「目标文件大小 / 原大小」比例:10→约 92%,40→约 60%,55→约 41%,70→约 28%。
*/
static long computeTargetBytesForSlider(long originalSize, int percent) {
if (originalSize <= 0) {
return 4096;
}
int p = normalizeCompressPercent(percent);
double retention = 0.92 - (p - 10) / 60.0 * 0.64;
if (retention < 0.12) {
retention = 0.12;
}
long target = (long) Math.ceil(originalSize * retention);
target = Math.min(target, originalSize - 1);
target = Math.max(target, 4096L);
if (target >= originalSize) {
target = Math.max(4096L, originalSize * 3 / 4);
}
return target;
}
/** 压缩强度 10(较轻)~70(较重),超出范围则钳制。 */
public static int normalizeCompressPercent(int raw) {
int p = raw;
if (p < 10) {
p = 10;
}
if (p > 70) {
p = 70;
}
return p;
}
private static byte[] compressToTargetBytes(Path tempIn, String ext, long targetBytes) throws Exception {
String lower = ext.toLowerCase(Locale.ROOT);
if (".jpg".equals(lower) || ".jpeg".equals(lower)) {
return jpegQualitySearch(tempIn, targetBytes, ".jpg");
}
if (".png".equals(lower)) {
return pngTryCompress(tempIn, targetBytes);
}
if (".webp".equals(lower)) {
return webpQualitySearch(tempIn, targetBytes);
}
if (".gif".equals(lower)) {
return gifTryCompress(tempIn, targetBytes);
}
if (".bmp".equals(lower)) {
return bmpTryCompress(tempIn, targetBytes);
}
Path tmpOut = Files.createTempFile("imc_fb_", outputExtensionForBase(ext));
try {
runMagickBase(tempIn, tmpOut, ext);
return Files.readAllBytes(tmpOut);
} finally {
deleteQuietly(tmpOut);
}
}
private static byte[] jpegQualitySearch(Path tempIn, long targetBytes, String outSuffix) throws Exception {
byte[] best = null;
long bestLen = Long.MAX_VALUE;
for (int q = 92; q >= 18; q -= 3) {
Path out = Files.createTempFile("imc_j_", outSuffix);
try {
runMagick(out, buildCmdJpeg(tempIn, out, q));
byte[] bytes = Files.readAllBytes(out);
if (bytes.length <= targetBytes) {
return bytes;
}
if (bytes.length < bestLen) {
bestLen = bytes.length;
best = bytes;
}
} finally {
deleteQuietly(out);
}
}
long kb = Math.max(1L, targetBytes / 1024L);
Path extentOut = Files.createTempFile("imc_je_", outSuffix);
try {
runMagick(extentOut, buildCmdJpegExtent(tempIn, extentOut, kb));
byte[] bytes = Files.readAllBytes(extentOut);
if (bytes.length <= targetBytes) {
return bytes;
}
if (bytes.length < bestLen) {
bestLen = bytes.length;
best = bytes;
}
} catch (Exception ignored) {
// extent 在部分版本/图上可能不可用,忽略并走缩放
} finally {
deleteQuietly(extentOut);
}
double[] scales = {0.9, 0.75, 0.6, 0.45, 0.3};
for (double s : scales) {
Path scaled = Files.createTempFile("imc_sc_", ".jpg");
try {
runMagick(scaled, buildCmdScaleToJpeg(tempIn, scaled, s, 82));
byte[] trySmaller = jpegQualitySearch(scaled, targetBytes, ".jpg");
if (trySmaller.length <= targetBytes) {
return trySmaller;
}
if (trySmaller.length < bestLen) {
bestLen = trySmaller.length;
best = trySmaller;
}
} finally {
deleteQuietly(scaled);
}
}
return best != null ? best : Files.readAllBytes(tempIn);
}
private static byte[] webpQualitySearch(Path tempIn, long targetBytes) throws Exception {
byte[] best = null;
long bestLen = Long.MAX_VALUE;
for (int q = 95; q >= 10; q -= 5) {
Path out = Files.createTempFile("imc_w_", ".webp");
try {
runMagick(out, buildCmdWebp(tempIn, out, q));
byte[] bytes = Files.readAllBytes(out);
if (bytes.length <= targetBytes) {
return bytes;
}
if (bytes.length < bestLen) {
bestLen = bytes.length;
best = bytes;
}
} finally {
deleteQuietly(out);
}
}
double[] scales = {0.9, 0.75, 0.6, 0.45, 0.3};
for (double s : scales) {
for (int q = 85; q >= 10; q -= 5) {
Path scaled = Files.createTempFile("imc_ws_", ".webp");
try {
runMagick(scaled, buildCmdScaleWebp(tempIn, scaled, s, q));
byte[] bytes = Files.readAllBytes(scaled);
if (bytes.length <= targetBytes) {
return bytes;
}
if (bytes.length < bestLen) {
bestLen = bytes.length;
best = bytes;
}
} finally {
deleteQuietly(scaled);
}
}
}
return best != null ? best : Files.readAllBytes(tempIn);
}
private static byte[] pngTryCompress(Path tempIn, long targetBytes) throws Exception {
byte[] best = null;
long bestLen = Long.MAX_VALUE;
Path out = Files.createTempFile("imc_p_", ".png");
try {
runMagick(out, buildCmdPng(tempIn, out, 9));
byte[] bytes = Files.readAllBytes(out);
best = bytes;
bestLen = bytes.length;
if (bytes.length <= targetBytes) {
return bytes;
}
} finally {
deleteQuietly(out);
}
double[] scales = {0.95, 0.85, 0.7, 0.55, 0.4, 0.3};
for (double s : scales) {
Path scaled = Files.createTempFile("imc_ps_", ".png");
try {
runMagick(scaled, buildCmdScalePng(tempIn, scaled, s));
byte[] bytes = Files.readAllBytes(scaled);
if (bytes.length < bestLen) {
bestLen = bytes.length;
best = bytes;
}
if (bytes.length <= targetBytes) {
return bytes;
}
} finally {
deleteQuietly(scaled);
}
}
return best != null ? best : Files.readAllBytes(tempIn);
}
private static byte[] gifTryCompress(Path tempIn, long targetBytes) throws Exception {
byte[] best = null;
long bestLen = Long.MAX_VALUE;
Path out = Files.createTempFile("imc_g_", ".gif");
try {
runMagick(out, buildCmdGif(tempIn, out));
byte[] bytes = Files.readAllBytes(out);
best = bytes;
bestLen = bytes.length;
if (bytes.length <= targetBytes) {
return bytes;
}
} finally {
deleteQuietly(out);
}
double[] scales = {0.9, 0.75, 0.6, 0.45, 0.3};
for (double s : scales) {
Path scaled = Files.createTempFile("imc_gs_", ".gif");
try {
runMagick(scaled, buildCmdScaleGif(tempIn, scaled, s));
byte[] bytes = Files.readAllBytes(scaled);
if (bytes.length < bestLen) {
bestLen = bytes.length;
best = bytes;
}
if (bytes.length <= targetBytes) {
return bytes;
}
} finally {
deleteQuietly(scaled);
}
}
return best != null ? best : Files.readAllBytes(tempIn);
}
private static byte[] bmpTryCompress(Path tempIn, long targetBytes) throws Exception {
byte[] best = null;
long bestLen = Long.MAX_VALUE;
Path out = Files.createTempFile("imc_bm_", ".bmp");
try {
runMagick(out, buildCmdBmp(tempIn, out));
byte[] bytes = Files.readAllBytes(out);
best = bytes;
bestLen = bytes.length;
if (bytes.length <= targetBytes) {
return bytes;
}
} finally {
deleteQuietly(out);
}
double[] scales = {0.95, 0.85, 0.7, 0.55, 0.4, 0.3};
for (double s : scales) {
Path scaled = Files.createTempFile("imc_bms_", ".bmp");
try {
runMagick(scaled, buildCmdScaleBmp(tempIn, scaled, s));
byte[] bytes = Files.readAllBytes(scaled);
if (bytes.length < bestLen) {
bestLen = bytes.length;
best = bytes;
}
if (bytes.length <= targetBytes) {
return bytes;
}
} finally {
deleteQuietly(scaled);
}
}
return best != null ? best : Files.readAllBytes(tempIn);
}
private static void runMagickBase(Path tempIn, Path tempOut, String ext) throws Exception {
String lower = ext.toLowerCase(Locale.ROOT);
List<String> cmd;
if (".jpg".equals(lower) || ".jpeg".equals(lower)) {
cmd = buildCmdJpeg(tempIn, tempOut, 85);
} else if (".png".equals(lower)) {
cmd = buildCmdPng(tempIn, tempOut, 9);
} else if (".webp".equals(lower)) {
cmd = buildCmdWebp(tempIn, tempOut, 78);
} else if (".bmp".equals(lower)) {
cmd = buildCmdBmp(tempIn, tempOut);
} else if (".gif".equals(lower)) {
cmd = buildCmdGif(tempIn, tempOut);
} else {
cmd = buildCmdBaseToJpeg(tempIn, tempOut, 85);
}
runMagick(tempOut, cmd);
}
private static List<String> buildCmdJpeg(Path in, Path out, int quality) {
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add("-quality");
c.add(String.valueOf(quality));
c.add(out.toAbsolutePath().toString());
return c;
}
/** 尝试用 JPEG extent 约束体积(部分 ImageMagick 版本支持)。 */
private static List<String> buildCmdJpegExtent(Path in, Path out, long extentKb) {
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add("-define");
c.add("jpeg:extent=" + extentKb + "kb");
c.add(out.toAbsolutePath().toString());
return c;
}
private static List<String> buildCmdBaseToJpeg(Path in, Path out, int quality) {
return buildCmdJpeg(in, out, quality);
}
private static List<String> buildCmdScaleToJpeg(Path in, Path out, double factor, int quality) {
String resize = String.format(Locale.ROOT, "%.2f%%", factor * 100.0);
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add("-resize");
c.add(resize);
c.add("-quality");
c.add(String.valueOf(quality));
c.add(out.toAbsolutePath().toString());
return c;
}
private static List<String> buildCmdPng(Path in, Path out, int compressionLevel) {
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add("-define");
c.add("png:compression-level=" + compressionLevel);
c.add(out.toAbsolutePath().toString());
return c;
}
private static List<String> buildCmdScalePng(Path in, Path out, double factor) {
String resize = String.format(Locale.ROOT, "%.2f%%", factor * 100.0);
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add("-resize");
c.add(resize);
c.add("-define");
c.add("png:compression-level=9");
c.add(out.toAbsolutePath().toString());
return c;
}
private static List<String> buildCmdWebp(Path in, Path out, int quality) {
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add("-quality");
c.add(String.valueOf(quality));
c.add(out.toAbsolutePath().toString());
return c;
}
private static List<String> buildCmdBmp(Path in, Path out) {
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add(out.toAbsolutePath().toString());
return c;
}
private static List<String> buildCmdScaleBmp(Path in, Path out, double factor) {
String resize = String.format(Locale.ROOT, "%.2f%%", factor * 100.0);
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add("-resize");
c.add(resize);
c.add(out.toAbsolutePath().toString());
return c;
}
private static List<String> buildCmdScaleWebp(Path in, Path out, double factor, int quality) {
String resize = String.format(Locale.ROOT, "%.2f%%", factor * 100.0);
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add("-resize");
c.add(resize);
c.add("-quality");
c.add(String.valueOf(quality));
c.add(out.toAbsolutePath().toString());
return c;
}
private static List<String> buildCmdScaleGif(Path in, Path out, double factor) {
String resize = String.format(Locale.ROOT, "%.2f%%", factor * 100.0);
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add("-coalesce");
c.add("-resize");
c.add(resize);
c.add("-layers");
c.add("optimize");
c.add(out.toAbsolutePath().toString());
return c;
}
private static List<String> buildCmdGif(Path in, Path out) {
List<String> c = new ArrayList<>();
c.add(ImageMagickRunUtils.magickBinary());
c.add(in.toAbsolutePath().toString());
c.add("-coalesce");
c.add("-layers");
c.add("optimize");
c.add(out.toAbsolutePath().toString());
return c;
}
private static String outputExtensionForBase(String ext) {
return ext;
}
private static void runMagick(Path expectedOut, List<String> cmd) throws Exception {
ImageMagickRunUtils.runMagick(expectedOut, cmd);
}
private static void deleteQuietly(Path p) {
if (p == null) {
return;
}
try {
Files.deleteIfExists(p);
} catch (IOException ignored) {
// ignore
}
}
private static String suffixOf(String filename) {
int i = filename.lastIndexOf('.');
return i >= 0 ? filename.substring(i).toLowerCase(Locale.ROOT) : ".jpg";
}
}