草庐IT

拒绝卡脖子 | 实现自己的图片压缩工具,想怎么压就怎么压

石添的编程哲学 2024-01-11 原文

当需要压缩图片时,到百度上搜索发现都是广告,需要下载软件和收费,贫穷不允许我这么任性。

好不容易找到一个在线免费压缩图片的网站,又有各种各样的限制,比如我一直在用的在线免费压缩工具【tinypng】,数量限制无所谓,但是图片最大5M,往往不能满足需求,如果需要使用高级功能需要付费。

当我找UI美眉帮忙把图片处理小点时,嗯哼~~~,何不自己开发一个图片处理工具。

目前软件都会上传图片,一般都会对用户上传的图片大小进行限制,并且上传之后还要压缩,这也可以为企业节省存储成本的同时,还可提高上传速率。

本文就基于SpringBoot结合thumbnailator实现图片压缩,坑已踩过拿去就用,也可以自己开发一个压缩工具不被卡脖子。

本文大纲

  • 基于 thumbnailator 实现图片压缩,添加水印,旋转等常用功能
  • 踩坑压缩图片反而变大的现象,并给出解决方案

创建SpringBoot工程

创建SpringBoot工程并引入相关依赖,根据Maven仓库地址搜索最新版本为0.4.19,因0.4.8版本利用率最高,本工程也是用0.4.8版本,你可以自行选择版本使用。

pom依赖

<!-- springboot-web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.4</version>
</dependency>
<!-- 图片压缩依赖 -->
<dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.8</version>
</dependency>
<!-- 测试模块 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
</dependency>

压缩文件工具类

上传文件时接口使用MultipartFile接收文件,所以我们压缩图片的方法也接收MultipartFile类型数据,并返回MultipartFile类型的数据【可根据实际情况调整参数类型】。

package com.stt.thumbnailator.util;

import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * 图片处理工具类
 */
@Component
@Slf4j
public class ImgUtil {

    /**
     * 压缩图片,不修改尺寸
     * 1、接收 file 源文件,并判断是否为空
     * 2、不修改原尺寸,按比例压缩图片
     * 3、将压缩后的图片封装为 MultipartFile 类型返回
     * @param file
     * @return
     */
    public MultipartFile zipImg(MultipartFile file) {
        // 判空,并且大于20kb再压缩
        if(file == null || file.getSize() <= 20 * 1024) {
            return file;
        }
        // 根据输入流压缩
        log.info("压缩前图片大小===》{}",file.getSize());
        // 字节文件输出流,保存转换后的图片数据流
        ByteArrayOutputStream outputStream = null;
        // 通过输入流转换为 MultipartFile
        ByteArrayInputStream inputStream = null;
        try {
            outputStream = new ByteArrayOutputStream();
            Thumbnails.of(file.getInputStream())
                    .scale(1f) //按比例放大缩小 和size() 必须使用一个 不然会报错
                    .outputQuality(0.5f) //输出的图片质量  0~1 之间,否则报错
                    .toOutputStream(outputStream); //图片输出位置
            // 将 outputStream 转换为 MultipartFile
            byte[] bytes = outputStream.toByteArray();
            inputStream = new ByteArrayInputStream(bytes);
            // 创建 MockMultipartFile 对象,该类在【spring-test】依赖中
            MockMultipartFile outFile = new MockMultipartFile(file.getOriginalFilename(), file.getOriginalFilename(), file.getContentType(), inputStream);
            log.info("压缩后图片大小===》{}",outFile.getSize());
            // 返回图片
            return outFile;
        } catch (IOException e) {
            log.error("图片压缩失败===》{}",e);
            throw new RuntimeException(e);
        }finally {
            // 关闭流
            try {
                outputStream.close();
                inputStream.close();
            } catch (IOException e) {
            }
        }
    }
}

上传文件接口

package com.stt.thumbnailator.controller;

import com.stt.thumbnailator.util.ImgUtil;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
 * 上传文件接口
 */
@RestController
@RequestMapping("upload")
public class UploadController {

    private final ImgUtil imgUtil;

    public UploadController(ImgUtil imgUtil) {
        this.imgUtil = imgUtil;
    }

    /**
     * 上传图片,请求方式需要是POST请求
     * @param file
     */
    @PostMapping("img")
    public String uploadImg(MultipartFile file) {
        // 上传图片
        MultipartFile zipImg = imgUtil.zipImg(file);
        return "压缩成功";
    }
}

application.yml

修改配置类默认文件上传大小,默认1M根本不够用。

spring:
  servlet:
    multipart:
      # 单个文件最大大小,默认1MB,该值根据实际需求调整
      max-file-size: 5MB
      # 一次请求文件最大大小,默认10MB,该值根据实际需求调整
      max-request-size: 20MB

测试

通过apifox快速发送请求,你也可以使用postman等工具。

发现控制台输出压缩前和压缩后文件大小,压缩将近3倍,由outputQuality方法参数控制,值越小压缩越严重,当然要保障图片的清晰度,和产品需求适当调整。

亲测在 0.3 时图片会出现模糊

压缩图片并存储

图片一般都会存储到文件服务器,这里暂时将图片通过IO流存储到本地,来看一下图片清晰度。

/**
  * 压缩图片,不修改尺寸,存储到本地
  * @param file
*/
public void zipImgToLocation(MultipartFile file) {
    // 判空,并且大于20kb再压缩
    if(file == null || file.getSize() <= 20 * 1024) {
        return;
    }
    // 字节文件输出流,保存转换后的图片数据流
    ByteArrayOutputStream outputStream = null;
    // 获取文件名
    String originalFilename = file.getOriginalFilename();
    // 获取文件后缀
    String fileType = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
    // 存储文件位置和名字,改名字一般都随机生成
    File localFile = new File("zipImg." + fileType);
    try {
        outputStream = new ByteArrayOutputStream();
        Thumbnails.of(file.getInputStream())
            .scale(1f) //按比例放大缩小 和size() 必须使用一个 不然会报错
            .outputQuality(0.5f) //输出的图片质量  0~1 之间,否则报错
            .toFile(localFile);
    } catch (IOException e) {
        log.error("图片压缩失败===》{}",e);
        throw new RuntimeException(e);
    }finally {
        // 关闭流
        try {
            outputStream.close();
        } catch (IOException e) {
        }
    }
}

在个人电脑前看着并没有差别,从3.68M压缩到1.05M,这张图上传到头条之后估计还得被平台压缩,总之可以根据outputQuality参数调整压缩程度,别太模糊就行。

压缩变大

压缩也有坑啊,比如下方代码,如果你将一张jpg的图片按照原0.5的压缩比压缩后通过outputFormat方法转换成png格式,图片反而会被增大。

/**
  * 压缩图片,不修改尺寸,修改为png格式
  * @param file
*/
public void zipImgToPng(MultipartFile file) {
    // 判空,并且大于20kb再压缩
    if(file == null || file.getSize() <= 20 * 1024) {
        return;
    }
    // 根据输入流压缩
    log.info("压缩前图片大小===》{}",file.getSize());
    // 字节文件输出流,保存转换后的图片数据流
    ByteArrayOutputStream outputStream = null;
    try {
        outputStream = new ByteArrayOutputStream();
        Thumbnails.of(file.getInputStream())
            .scale(1f) //按比例放大缩小 和size() 必须使用一个 不然会报错
            .outputQuality(0.5f) //输出的图片质量  0~1 之间,否则报错
            .outputFormat("png") // 修改图片为png格式
            .toOutputStream(outputStream);
        log.info("压缩后图片大小===》{}",outputStream.size());
    } catch (IOException e) {
        log.error("图片压缩失败===》{}",e);
        throw new RuntimeException(e);
    }finally {
        // 关闭流
        try {
            outputStream.close();
        } catch (IOException e) {
        }
    }
}

图片由3M增大到了20M

相似的问题,不少小伙伴也都遇到了

该问题在2022年12月31号更新的0.4.19版本中也并没有解决,所以只能自行处理,也就有大佬总结出以下规则,Thumbnails.scale效果会导致图片大小变大。

根据多次测试得来的结果:用jpg转成jpg效果最佳

  • 当图片为jpg时不要转换直接压缩
  • 当图片为png时也可以直接压缩,不要转换
  • 当图片为png时可以先转换为jpg再压缩也可以

添加水印

比如有以下水印,添加到任意一张图片的右上角

工具类

/**
  * 添加水印
  * file: 原图
  * markFile:水印
*/
public void addWatermark(MultipartFile file,MultipartFile markFile) {
    // 判空
    if(file == null) {
        return;
    }
    // 原图文件流
    try {
        InputStream inputStream = file.getInputStream();
        InputStream markFileInputStream = markFile.getInputStream();
        // 读取数据
        BufferedImage srcImg = ImageIO.read(inputStream);
        BufferedImage markImg = ImageIO.read(markFileInputStream);
        //原图的宽高
        int srcWidth = srcImg.getWidth(null);
        int srcHeight = srcImg.getHeight(null);
        //水印图片的宽高
        int markWidth = markImg.getWidth(null);
        int markHeight = markImg.getHeight(null);

        //计算输出水印图片的位置x和y轴
        int mark_x = srcWidth - srcWidth / 9;
        int mark_y = srcWidth / 9-srcWidth / 10;
        //计算输出水印图片的大小
        int mark_width = srcWidth / 10;
        int mark_height = (srcWidth * markHeight) / (10 * markWidth);

        //将水印图片压缩成输出的大小
        markImg = Thumbnails.of(markImg).size(mark_width,mark_height).asBufferedImage();

        String originalFilename = file.getOriginalFilename();
        // 获取文件后缀
        String fileType = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
        File finalFile = new File("finalFile." + fileType);
        //watermark(位置,水印图,透明度0.5f=50%透明度)
        //outputQuality(控制图片的质量,1f=100%高质量)
        Thumbnails.of(srcImg)
            .size(srcWidth, srcHeight)
            .watermark(new Coordinate(mark_x,mark_y), markImg, 1f)
            .outputQuality(1f)
            .toFile(finalFile);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

接口需要接收两张图片,你也尽可以使用多文件上传

@PostMapping("img/addWatermark")
public String addWatermark(@RequestParam("srcFile") MultipartFile srcFile,@RequestParam("markFile")  MultipartFile markFile) {
    // 上传图片
    imgUtil.addWatermark(srcFile,markFile);
    return "添加水印成功";
}

测试添加水印,发送请求时参数名一定要和接口中定义的@RequestParam 注解值相同

测试后发现右上角确实添加上了水印

图片旋转

可以通过 rotate 方法在顺时针和逆时针方向旋转图片

/**
  * 旋转图片
*/
public void rotate(MultipartFile file) {
    try {
        String originalFilename = file.getOriginalFilename();
        // 获取文件后缀
        String fileType = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
        File finalFile = new File("finalFile." + fileType);
        Thumbnails.of(file.getInputStream())
            .rotate(90) // 角度,正数:顺时针,负数:逆时针
            .toFile(finalFile);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

总结

  • 在压缩,旋转,添加水印,甚至裁剪时可以通过thumbnailator实现
  • 处理后的图片可以根据需求进行存储,也可以直接返回给客户端
  • 处理后的数据可以是一个File文件,也可以是一个数据流,根据不同需求而定
  • 你可以在此基础上开发自己的图片处理工具给朋友或企业使用,岂不非常哇塞!

这就是我在使用的图片处理技术,当然也有一些其他的方案,与其寻找各种方案哪种最优,不如挑选一种使用,进而自行优化,选择技术的路上往往会浪费许多时间,你觉得呢?

有关拒绝卡脖子 | 实现自己的图片压缩工具,想怎么压就怎么压的更多相关文章

  1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. ruby-on-rails - Ruby on Rails - 为文本区域和图片生成列 - 2

    我是Rails的新手,所以请原谅简单的问题。我正在为一家公司创建一个网站。那家公司想在网站上展示它的客户。我想让客户自己管理这个。我正在为“客户”生成一个表格,我想要的三列是:公司名称、公司描述和Logo。对于名称,我使用的是name:string但不确定如何在脚本/生成脚手架终端命令中最好地创建描述列(因为我打算将其设置为文本区域)和图片。我怀疑描述(我想成为一个文本区域)应该仍然是描述:字符串,然后以实际形式进行调整。不确定如何处理图片字段。那么……说来话长:我在脚手架命令中输入什么来生成描述和图片列? 最佳答案 对于“文本”数

  4. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  5. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  6. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  7. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  8. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  9. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  10. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

随机推荐