首页 > 编程语言 >极简 Java 图像处理教程:压缩、封装、编码让传输更轻松!

极简 Java 图像处理教程:压缩、封装、编码让传输更轻松!

时间:2024-11-16 20:18:59浏览次数:3  
标签:极简 Java String jpg inputStream return 图像处理 IOException new

1. 背景简介及目的

这是一个java读取图片流并进行格式转换,图片高质量压缩,ZIP格式转Base64的极简教程。功能需求是在对接农行支付接口时产生的,满足农行二级商户管理接口中上传商户影印件的功能。写这篇博客的目的是分享给大家我在工作中遇到的实际需求,解决问题的思路,以及一些技术探讨。对接银行的接口开发,他们就直接给文档,是不安排技术支持和联调的,先看一眼文档,目前我要解决的是二级商户证件照资料上传

通过查看报文用例,确认SubMerCertFile字段就是文件流对应的字段。字段类型要求是String,凭借经验,也能知道这个是Base64编码格式,银行的接口文档看着第一眼就觉得奇怪,要求的字段基本都是大写开头的,在现代开发框架中,实体类大写开头的话,可能会产生一些Bug,如spring通过@RequestParam、@PathVariable注入时,会导致找不到属性。

2.相关概念介绍:

Base64 是一种用于将二进制数据编码为 ASCII 字符串的编码方式。它广泛应用于需要以文本格式传输或存储二进制数据的场景,如电子邮件、数据传输、图像嵌入等。

2.1主要特点:

        编码原理

  • Base64 使用 64 个字符(A-Z、a-z、0-9、+、/)来表示数据。每 3 个字节的二进制数据会被编码为 4 个 ASCII 字符。
  • 为了保证输出总是可以被整齐地分割,Base64 会在数据末尾添加一个或两个等号(=)作为填充。

        可读性

  • Base64 编码的输出是可打印的 ASCII 字符,适合在文本环境中传输,避免了二进制数据可能带来的问题。

        应用场景

  • 常用于将图像、音频等文件嵌入 HTML、CSS 文件中,避免额外的 HTTP 请求。
  • 在电子邮件中,Base64 常用于编码附件,使其可以安全传输。

        编码和解码

  • 编码时,将每 3 个字节转为 4 个字符;解码时则反向操作,将 4 个字符还原为 3 个字节。

3.准备工作 

        3.1 引入依赖
      <!-- 高质量压缩图设置图片尺寸,转换图片格式-->                                    
      <thumbnailator.version>0.4.20</thumbnailator.version>
         <dependency>
            <groupId>net.coobird</groupId>
            <artifactId>thumbnailator</artifactId>
            <version>${thumbnailator.version}</version>
         </dependency>

4.整体思路

在我们框架中,已经存在公共的图片上传接口,主要是接收一个file流,指定一个分类(枚举值)用来将图片存到不同的存储桶(bucket),最后返回一个绝对路径URL地址,前端将这个地址作为我接口需要的path字段,最终存储到我的数据库的是一个相对路径字符串,之所以存相对路径,是因为存储服务器可能会存在迁移的情况,例如我们项目就是从阿里云迁移到移动云。以下是公共上传接口:

 /**
     * 上传图片
     *
     * @param multipartFiles
     * @return
     */
    @ApiOperation(value = "上传图片")
    @ApiImplicitParam(name="ify",value = "图片分类:1-商品,2-身份证,3-营业执照,4-app轮播图, 5-发票,6-银行回执单,7-资讯,8-门头照,9-店内照,10-保险单,11-银行卡正面,12-银行卡反面,13-商品主图,14-商品详情图",required = true,dataType = "integer")
    @PostMapping("/api/uploadImages")    
public Result<List<UploadVO>> uploadImages(@RequestParam("file") MultipartFile[] multipartFiles,@RequestParam Integer ify) throws IOException {
//上传文件操作
//非本文重点,此处省略
}

结合实际的用户交互,以及框架规范等约束,我直接给出我的思路:

tep1:前端指定ify字段值,调用公共上传组件传照片,并拿到接口返回的绝对路径URL字符串
tep2:用户填写完资料,将上一步拿到的url返回值提交给我的接口
tep3:接口解析完请求参数,使用hutool工具包复制即可,仅需处理URL字符串
tep4:我需要通过前端给的绝对路径,转成图片,这里选择调用EOS(移动云的对象存储)下载功能获取图片流(考虑到Http方式读取图片会有延迟问题影响体验感,还是直接用官方API)
tep5:将图片流转换成jpg(农行这个接口文档没说什么格式,但实际上只能是JPG,这是实战得出来结论,而且用户群体基本是经商的老年人,根本不懂什么JPG,顶多就是拍个照片上传,能独立完成这个用户界面操作就属于很厉害的了)
tep6:判断图片内存大小是否超过1.5M,若是,则采用简单的算术来压缩至1.5M以下
tep7:图片转成zip格式
tep8:zip转成base64,之所以这样细分,也是设计中的单一原则,一个方法就处理一件事情,可以做到最大程度复用。

5.具体的代码

5.1农行接口处理请求

不太喜欢用get,set方法来操作字段,因为代码行数非常多,看起来不够简介,对于第三方的接口,我比较倾向于使用各种序列化框架,直接就能通过名称来取值。

5.2通过eos提供的API获取流

参数是一个String类型,表示接收一个路径地址,然后使用API去连接云储存,再序列化成为java对象,接着就是对流的各种操作。

/**
     * 将eos对象存储转换成base64字符串
     * @param objectName
     * @return
     * @throws IOException
     */
    public String zip2Base64(String objectName) throws IOException {
        objectName = removeLeadingSlash(objectName);

        // 填写存储桶(Bucket)所在地域对应的 endpoint 和 Region。
        String endpoint = cloudEosProperties.getEndpoint();
        String region = cloudEosProperties.getRegion();
        String accessKey = cloudEosProperties.getAccessKeyId();
        String secretKey = cloudEosProperties.getAccessKeySecret();
        String bucketName = cloudEosProperties.getBucketName();

        // 创建 AmazonS3 实例。
        AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(endpoint, region);
        BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
        AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
        AmazonS3 client = AmazonS3ClientBuilder.standard()
                .withEndpointConfiguration(endpointConfiguration)
                .withCredentials(credentialsProvider).build();

        // 下载对象
        S3Object s3Object = client.getObject(bucketName, objectName);

        try (InputStream inputStream = s3Object.getObjectContent()) {
            // 压缩图像并返回压缩后的输入流
            InputStream compressedStream = compressStream(inputStream, "jpg", 1.0); // 格式为 "jpg",根据需要调整

            // 将压缩后的图像数据压缩成 ZIP 文件,并转换为 Base64 编码
            ByteArrayOutputStream zipOutputStream = zip(compressedStream, objectName, "jpg");
            byte[] zipBytes = zipOutputStream.toByteArray();
            return Base64.getEncoder().encodeToString(zipBytes);
        } catch (IOException e) {
            log.error("处理文件时出错", e);
            throw new RuntimeException("处理文件时出错", e);
        }
    }
5.3将图片转换成ZIP格式
/**
     * 将图片转换为zip格式
     * @param inputStream
     * @param name
     * @param format
     * @return
     * @throws IOException
     */
    public static ByteArrayOutputStream zip(InputStream inputStream, String name, String format) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (ZipOutputStream zos = new ZipOutputStream(baos)) {
            ZipEntry zipEntry = new ZipEntry(name + "." + format);
            zos.putNextEntry(zipEntry);

            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                zos.write(buffer, 0, bytesRead);
            }
            zos.closeEntry();
        } catch (IOException e) {
            throw new RuntimeException("压缩文件时出错", e);
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
        return baos;
    }
5.4图片压缩至1.5M以下
/**
     * 压缩图片至1.5兆以下
     * @param inputStream
     * @param format
     * @param scale
     * @return
     * @throws IOException
     */
    public static InputStream compressStream(InputStream inputStream, String format, double scale) throws IOException {
        // 读取 InputStream 到字节数组
        byte[] imageBytes = ByteStreams.toByteArray(inputStream);
        ByteArrayInputStream originalStream = new ByteArrayInputStream(imageBytes);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            // 压缩图像并写入到 ByteArrayOutputStream
            Thumbnails.of(originalStream)
                    .outputQuality(1)
                    .scale(scale)
                    .outputFormat(format)
                    .toOutputStream(baos);

            // 检查文件大小是否大于 1.5MB
            if (baos.size() > 1.5 * 1024 * 1024) {
                baos.reset(); // 清空输出流,重新压缩
                originalStream.reset(); // 重置输入流
                Thumbnails.of(originalStream)
                        .outputQuality(0.3)
                        .scale(scale)
                        .outputFormat(format)
                        .toOutputStream(baos);
            }
            // 返回 ByteArrayInputStream
            return new ByteArrayInputStream(baos.toByteArray());
        } catch (IOException e) {
            throw new RuntimeException("图片压缩或转换格式出错", e);
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            originalStream.close();
        }
    }
5.5去掉数据库中路径字段的开头字符
/**
     * 去掉相对路径的前缀 斜杠(/)
     * @param str
     * @return
     */
    public static String removeLeadingSlash(String str) {
        if (str != null && str.startsWith("/")) {
            return str.substring(1);
        }
        return str;
    }
5.6裁剪照片为指定像素
/**
     * 裁剪照片为指定像素
     * @param inputStream
     * @param targetWidth
     * @param targetHeight
     * @return
     * @throws IOException
     */
    public static InputStream generateMainImage(InputStream inputStream, Integer targetWidth, Integer targetHeight) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int length;

        // 将输入流读取到字节数组中
        while ((length = inputStream.read(buffer)) != -1) {
            byteArrayOutputStream.write(buffer, 0, length);
        }
        byte[] imageBytes = byteArrayOutputStream.toByteArray();

        // 获取图片尺寸
        //Pair<Integer, Integer> pair = getImageDimensions(imageBytes);
        /*int originalWidth = pair.getKey();
        int originalHeight = pair.getValue();*/

        // 检查图片尺寸
        /*if (originalWidth < 1000 || originalHeight < 1000) {
            throw new RuntimeException("请上传宽高同时大于1000像素的照片");
        }*/

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        try {
            // 如果目标高度为空,则保持等比缩放
            if (targetHeight == null) {
                Thumbnails.of(new ByteArrayInputStream(imageBytes))
                        .outputFormat("jpg")
                        .outputQuality(0.6)
                        .width(targetWidth)
                        .toOutputStream(outputStream);
            } else {
                // 先等比缩放,再进行居中裁剪
                Thumbnails.of(new ByteArrayInputStream(imageBytes))
                        .outputFormat("jpg")
                        .outputQuality(1)
                        .size(targetWidth, targetHeight) // 等比缩放
                        .crop(Positions.CENTER) // 从中心裁剪
                        .toOutputStream(outputStream);
            }

            return new ByteArrayInputStream(outputStream.toByteArray());
        } catch (IOException e) {
            throw new RuntimeException("裁剪图片出错", e);
        } finally {
            inputStream.close();  // 可选,视情况决定是否关闭流
        }
    }
5.7获取图片像素
// 获取图片尺寸
    public static Pair<Integer, Integer> getImageDimensions(byte[] bytes) throws IOException {
        BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
        int width = image.getWidth();
        int height = image.getHeight();
        return new Pair<>(width, height);
    }
5.8进一步封装
/**
     * 裁剪照片为指定的宽度,高度保持宽高比缩放
     * @param inputStream
     * @param targetWidth
     * @return
     * @throws IOException
     */
    public static InputStream generateMainImage(InputStream inputStream, int targetWidth) throws IOException {
        return generateMainImage(inputStream,targetWidth,null);
    }

6.关于Thumbnailator 库

Thumbnailator 是一个轻量级的 Java 库,专门用于图像处理。它的主要目标是简化图像缩放和其他操作,如裁剪、旋转、合成等。相对于其他常见的图像处理库(如 Java ImageIOApache Commons Imaging),Thumbnailator 更加简洁易用,并且具有更好的性能表现,尤其在图像缩放和调整方面。

7.Thumbnailator基本用法介绍

7.1使用 Thumbnailator 缩放图像
public static void main(String[] args) {
        try {
            // 读取原始图片文件,并生成缩略图
            Thumbnails.of(new File("path/to/original/image.jpg"))
                    .size(200, 200)  // 设置缩略图的宽度和高度
                    .toFile(new File("path/to/output/thumbnail.jpg"));
            System.out.println("缩略图生成成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
7.2 按比例缩放图像
Thumbnails.of(new File("path/to/original/image.jpg"))
    .size(200, 200)
    .keepAspectRatio(true)  // 保持原始比例
    .toFile(new File("path/to/output/thumbnail.jpg"));
7.3裁剪图像-固定区域裁剪
public static void main(String[] args) {
        try {
            Thumbnails.of(new File("path/to/original/image.jpg"))
                    .sourceRegion(100, 100, 300, 300)  // 设定裁剪区域,(x, y, width, height)
                    .size(200, 200)  // 之后将裁剪的区域缩放为200x200
                    .toFile(new File("path/to/output/cropped_image.jpg"));
            System.out.println("裁剪图像成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
7.4裁剪图像-中心裁剪
Thumbnails.of(new File("path/to/original/image.jpg"))
    .sourceRegion(Positions.CENTER, 200, 200)  // 从中心裁剪出200x200区域
    .toFile(new File("path/to/output/cropped_center_image.jpg"));
7.5旋转图像
Thumbnails.of(new File("path/to/original/image.jpg"))
    .rotate(90)  // 顺时针旋转90度
    .toFile(new File("path/to/output/rotated_image.jpg"));
7.6综合操作:缩放、裁剪、旋转
Thumbnails.of(new File("path/to/original/image.jpg"))
    .size(400, 400)  // 缩放图像为400x400
    .rotate(45)  // 旋转45度
    .sourceRegion(Positions.CENTER, 300, 300)  // 从中心裁剪出300x300区域
    .toFile(new File("path/to/output/final_image.jpg"));

8.总结

本文既是分享,也是巩固所学所用,运营电商项目,随时会更换商品图片,如果不做控制,上传上来的图片会不适应小程序或者APP,图片会糊。多一个工具,多一种解决问题的手段而已。

标签:极简,Java,String,jpg,inputStream,return,图像处理,IOException,new
From: https://blog.csdn.net/Ta20220617/article/details/143496254

相关文章

  • Java虚拟机JVM-程序计数器 讲解
    目录Java8的JVM内存结构程序计数器的功能程序技术器的具体细节class文件的字节码视图的内容程序计数器的特性Java8的JVM内存结构程序计数器的功能记录每个线程正在执行的字节码指令的地址,帮助JVM确定下一条需要执行的指令。程序技术器的具体细节class文件的......
  • JDBC学习笔记(四)--JdbcUtil工具类的底层实现、解决 java.sql.SQLException: Operation
    目录(一)为什么要使用JdbcUtil工具类(二)创建一个prorperties文件1.在文件目录或src目录下,选择新建FIle2.创建properties文件 3.编写配置文件Java基础:反射4.获取资源的方式第一种第二种 ​编辑 第三种(一)为什么要使用JdbcUtil工具类问题:在编写jdbc的时候,在每一......
  • springboot3整合mybatisplus问题Invalid value type for attribute 'factoryBeanObjec
    版本说明:JDK版本:17springboot版本:3.3.5问题分析:springboot版本与mybatisplus版本不兼容解决办法:将mybatisplus版本替换为以下版本<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>......
  • 基于Java+SSM+JSP+MYSQL实现的宠物领养收养管理系统功能设计与实现四
    一、前言介绍:免费学习:猿来入此1.1项目摘要随着人们生活水平的提高,宠物已经成为越来越多家庭的重要成员。然而,宠物的数量增长也带来了一系列问题,如流浪宠物数量的增加、宠物健康管理的缺失以及宠物领养收养信息的不透明等。这些问题不仅影响了宠物的生存状况,也给社会带来了一定......
  • 基于java+vue的广告管理系统设计与实现
    目录:目录:博主介绍: 完整视频演示:你应该选择我技术栈介绍:需求分析:系统各功能实现一览:1.注册2.登录部分代码参考: 项目功能分析: 项目论文:源码获取:博主介绍: ......
  • 基于Java+SSM+JSP+MYSQL实现的宠物领养收养管理系统功能设计与实现三
    一、前言介绍:免费学习:猿来入此1.1项目摘要随着人们生活水平的提高,宠物已经成为越来越多家庭的重要成员。然而,宠物的数量增长也带来了一系列问题,如流浪宠物数量的增加、宠物健康管理的缺失以及宠物领养收养信息的不透明等。这些问题不仅影响了宠物的生存状况,也给社会带来了一定......
  • 【全栈开发(TypeOrm-Javascript)学习笔记三】
    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录前言一、EntityManager二、Repository三、Find选项四、自定义Repository五、EntityManagerAPI六、RepositoryAPI总结前言本章节主要了解typeOrm框架EntityManager和Repository,学习常见的s......
  • java学习笔记-面向对象-类的内部构造与对象(2)
    类是一组具有相同属性和行为的对象的抽象。类及类的关系构成了对象模型的主要内容。面向对象编程的主要任务就是定义对象模型中的各个类。1.定义类(class)//定义静态属性--班费//在类中被定义为静态的属性将被所有该类创建的对象所共享staticdoubleclass......
  • [leetcode]27. 移除元素(Java实现)
    题目给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:更改 nums 数组,使 nums 的前 k ......
  • [leetcode]485. 最大连续1的个数(Java实现)
    题目给定一个二进制数组 nums ,计算其中最大连续 1 的个数。示例1:输入:nums=[1,1,0,1,1,1]输出:3解释:开头的两位和最后的三位都是连续1,所以最大连续1的个数是3.示例2:输入:nums=[1,0,1,1,0,1]输出:2解法1嗯,速度有进步我感觉我想出这个思路还是挺快的,两......