SpringBoot实现图片添加水印

news2024/11/15 17:24:12

提示:今日完成图片添加水印功能

后续可能还会继续完善这个功能

文章目录

目录

文章目录

 前端部分

后端

Xml

Controller层

Sercive层

Service实现层

 Config配置层

application.properties

文件后缀名获取

常量定义



 前端部分

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload</title>
</head>
<body>
<h1>上传图片</h1>
<form method="post" enctype="multipart/form-data" action="http://localhost:8080/api/upload">
    <input type="file" name="file" />
    <button type="submit">上传</button>
</form>
</body>
</html>

后端

Xml

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>
        <dependency>
            <groupId>net.coobird</groupId>
            <artifactId>thumbnailator</artifactId>
            <version>0.4.8</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

Controller层

@RequestMapping("/upload")
    public ResponseVO upload(@RequestParam("file")MultipartFile file){
        if(null == file){
            throw new BusinessException("上传文件不能为空");
        }
        uploadService.upload(file);
        return getSuccessResponseVO("上传成功");
    }

Sercive层

public interface UploadService {
    void upload(MultipartFile file);
}

Service实现层

@Service("UploadService")
public class UploadServiceImpl implements UploadService {

    private static final Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);

    private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList("jpg");

    @Resource
    private AppConfig appConfig;
    /**
     * 上传文件。
     *
     * @param file 上传的文件对象。
     * @throws BusinessException 如果文件格式不被允许或上传过程中出现错误。
     */
    @Override
    public void upload(MultipartFile file) {
        // 构建文件存储路径
        String projectFolder = appConfig.getProjectFolder()+ Constants.FILE_PATH;
        // 获取文件原始名称
        String fileName = file.getOriginalFilename();
        // 检查文件格式是否被允许
        Boolean allowed = isAllowed(fileName);
        if(!allowed){
            throw new BusinessException("文件格式错误,请上传jpg,png,webp,jpeg,gif等图片格式");
        }
        // 获取当前日期,用于构建日期目录
        LocalDate now = LocalDate.now();
        // 根据日期格式化字符串
        String datePath = now.format(DateTimeFormatter.ofPattern(DateTimePatternEnum.YYYY_MM.getPattern()));
        // 构建上传文件的项目文件夹路径
        File uploadFileProjectFolder = new File(projectFolder + "/"+datePath);
        // 如果文件夹不存在,则创建文件夹
        if(!uploadFileProjectFolder.exists()){
            uploadFileProjectFolder.mkdirs();
        }
        // 移除文件扩展名,用于构建文件夹名称
        String fileNameWithout = fileName.substring(0,fileName.lastIndexOf("."));
        // 构建文件夹路径
        File fileFolder = new File(uploadFileProjectFolder.getPath() + "/" + fileNameWithout);
        // 如果文件夹不存在,则创建文件夹
        if(!fileFolder.exists()){
            fileFolder.mkdirs();
        }
        // 构建新文件路径
        File newFile = new File(fileFolder.getPath() + "/" + fileName);
        // 获取文件扩展名
        String suffix = StringUtil.getSuffix(fileName);
        // 构建带水印的文件名
        //带水印版本的名字
        String watermarkName = fileNameWithout+"_"+suffix;
        // 创建带水印的文件对象
        //创建带水印且压缩画质的版本
        File newWatermarkName = new File(fileFolder.getPath() + "/" + watermarkName);
        try {
            // 将上传的文件保存到服务器
            file.transferTo(newFile);
        } catch (IOException e) {
            logger.error("上传文件失败:{}",e);
            throw new BusinessException("上传文件失败");
        }
        try {
            // 为文件添加水印并压缩
            addWatermarkAndCompress(newFile, newWatermarkName,"贺浩浩");
        } catch (IOException e) {
            logger.error("保存水印版本失败:{}",e);
            throw new BusinessException("保存水印版本失败");
        }

        logger.info("projectFolder:{}",projectFolder);
    }


    /**
     * 检查文件名是否允许。
     *
     * 此方法通过检查文件名的扩展名来确定文件名是否被允许。文件名必须包含至少一个点(.)
     * 以标识扩展名,并且扩展名必须在预定义的允许扩展名列表中。
     *
     * @param fileName 要检查的文件名。
     * @return 如果文件名的扩展名被允许,则返回true;否则返回false。
     */
    private Boolean isAllowed(String fileName) {
        // 检查文件名是否为空或不包含点(.)
        if(fileName == null || fileName.lastIndexOf(".") == -1){
            return false;
        }
        // 提取文件名的扩展名,并转换为小写
        String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
        // 检查提取的扩展名是否在允许的扩展名列表中
        return ALLOWED_EXTENSIONS.contains(extension);
    }




    /**
     * 给图片添加水印并压缩保存。
     *
     * 此方法接收原始图片文件、目标输出文件和水印文本作为参数,它将原始图片读入,
     * 添加水印后,按照原始尺寸进行压缩,并保存到目标文件中。
     *
     * @param originalFile 原始图片文件,添加水印和压缩的基础文件。
     * @param outputFile 添加水印并压缩后的图片保存位置。
     * @param watermarkText 要添加的水印文本。
     * @throws IOException 如果读取或写入文件发生错误。
     */
    private void addWatermarkAndCompress(File originalFile, File outputFile,String watermarkText) throws IOException {
        // 读取原始图片
        BufferedImage originalImage = ImageIO.read(originalFile);
        // 添加水印
        BufferedImage watermarkedImage = addWatermark(originalImage, watermarkText);
        // 获取原图尺寸,以保持压缩后的图片尺寸与原图相同
        // 获取原图尺寸
        int originalWidth = originalImage.getWidth();
        int originalHeight = originalImage.getHeight();

        // 使用Thumbnails.of方法对添加水印后的图片进行缩放和压缩
        // 按比例缩放并压缩
        Thumbnails.of(watermarkedImage)
                .size(originalWidth, originalHeight)
                .outputQuality(0.4) // 调整画质压缩
                .toFile(outputFile);
    }


    /**
     * 给图片添加水印。
     *
     * @param image 原始图片。
     * @param watermarkText 水印文本。
     * @return 添加了水印的图片。
     */
    private BufferedImage addWatermark(BufferedImage image, String watermarkText) {
        // 获取原始图片的宽度和高度
        int imageWidth = image.getWidth();
        int imageHeight = image.getHeight();
        // 创建Graphics2D对象,用于在图片上绘制水印
        // 创建用于绘制水印的Graphics2D对象
        Graphics2D g2d = (Graphics2D) image.getGraphics();
        // 设置透明度,使水印呈现半透明效果
        // 设置水印的属性
        AlphaComposite alphaChannel = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
        g2d.setComposite(alphaChannel);
        // 设置水印文字颜色为灰色
        g2d.setColor(Color.GRAY);
        // 设置水印文字的字体、大小和样式
        // 使用支持中文的字体,例如SimHei(黑体)
        Font font = new Font("SimHei", Font.BOLD, 36);
        g2d.setFont(font);
        // 获取水印文字的尺寸信息
        FontMetrics fontMetrics = g2d.getFontMetrics();
        Rectangle2D rect = fontMetrics.getStringBounds(watermarkText, g2d);
        int textWidth = (int) rect.getWidth();
        int textHeight = (int) rect.getHeight();
        // 用于随机生成水印位置偏移量
        Random random = new Random();
        // 平铺方式添加水印,通过控制行间距和文字在行内的偏移,实现错落有致的布局效果
        // 平铺方式添加水印,单双行错开并随机偏移
        for (int y = 0; y < imageHeight; y += textHeight + 100) {
            // 判断当前行为偶数行还是奇数行,奇数行文字向右偏移
            boolean oddRow = (y / (textHeight + 100)) % 2 == 0;
            for (int x = oddRow ? 0 : textWidth / 2; x < imageWidth; x += textWidth + 300) {
                // 随机生成水平和垂直偏移量,使水印位置略有变化,避免整齐排列
                int xOffset = random.nextInt(100) - 50; // 随机偏移 -50 到 50 像素
                int yOffset = random.nextInt(50) - 25;  // 随机偏移 -25 到 25 像素
                // 在图片上绘制水印文字,位置略有偏移
                g2d.drawString(watermarkText, x + xOffset, y + yOffset);
            }
        }
        // 释放Graphics2D资源
        g2d.dispose();
        // 返回添加了水印的图片
        return image;
    }
}

由于只是一个简单的demo练习,所以并未使用到数据库,等待后续有时间出一个完整版本

 Config配置层

@Component
public class AppConfig {
    @Value("${project.folder:}")
    private String projectFolder;
    public String getProjectFolder() {
        return projectFolder;
    }
}

application.properties

# 应用服务 WEB 访问端口
server.port=8080
server.servlet.context-path=/api

#文件大小配置
spring.servlet.multipart.max-file-size=15MB
spring.servlet.multipart.max-request-size=15MB

#项目目录
project.folder=F:/a-xxxxxxxxxxxxxxxxx/java/SpringBoot_practice/mys

文件后缀名获取

public static String getSuffix(String fileName){
        String suffix = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
        return suffix;
    }

常量定义

/**
 * Constants类用于定义应用程序中使用的常量。
 * 该类中的常量应该是整个应用程序范围内不变的值。
 */
public class Constants {
    /**
     * 文件路径常量。
     * 该常量定义了访问文件系统的根路径。
     * 使用此常量可以确保应用程序中对文件路径的引用具有一致性和可维护性。
     */
    public static final String FILE_PATH = "/file";
}

上传结果

名字里面带下划线的是水印版本(原图)

不带的是无水印版本

 

 添加水印之后的版本

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1883572.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MD5算法详解

哈希函数 是一种将任意输入长度转变为固定输出长度的函数。 一些常见哈希函数有&#xff1a;MD5、SHA1、SHA256。 MD5算法 MD5算法是一种消息摘要算法&#xff0c;用于消息认证。 数据存储方式&#xff1a;小段存储。 数据填充 首先对我们明文数据进行处理&#xff0c;使其…

ARCGIS python 裁剪栅格函数 arcpy.management.Clip

ARCGIS python 裁剪栅格函数 arcpy.management.Clip 1 功能 裁剪掉栅格数据集、镶嵌数据集或图像服务图层的一部分。 2 使用情况 基于模板范围提取部分栅格数据集&#xff0c;输出与模板范围相交的所有像素使用以 x 和 y 坐标的最小值和最大值确定的包络矩形或使用输出范围文…

全国产化飞腾模块BIOS下修复系统启动文件

1、背景介绍 全国产飞腾模块采用麒麟信安操作系统&#xff0c;当系统下面的grub.cfg文件被用户误操作导致无法启动时&#xff0c;可以在BIOS下通过U盘中备份的grub.cfg替换硬盘上原来的grub.cfg文件&#xff0c;从而实现启动。 2、操作步骤 首先进入BIOS命令行模式&#xff…

使用 Rustup 管理 Rust 版本

文章目录 安装 Rustup配置镜像源安装 Rustup 安装 RustVS Code插件创建项目代码示例 Rust 官网&#xff1a;https://www.rust-lang.org/zh-CN/Crates 包管理&#xff1a;https://crates.io/Rust 程序设计语言&#xff1a;https://kaisery.github.io/trpl-zh-cn/通过例子学 Rust…

七人互助拼团模式的深度剖析与互助精神的价值

在当下电商的浪潮中&#xff0c;七人互助拼团模式以其独特的运作方式和互助精神&#xff0c;赢得了广大消费者和商家的青睐。这一模式不仅为用户提供了优惠的购物体验&#xff0c;更在无形中培养了用户间的互助与协作精神。 一、激励机制的细致解读 七人互助拼团模式中&#x…

GPT-4o文科成绩超一本线,理科为何表现不佳?

目录 01 评测榜单 02 实际效果 什么&#xff1f;许多大模型的文科成绩竟然超过了一本线&#xff0c;还是在竞争激烈的河南省&#xff1f; 没错&#xff0c;最近有一项大模型“高考大摸底”评测引起了广泛关注。 河南高考文科今年的一本线是521分&#xff0c;根据这项评测&…

notepad++安装并打开json文件

1、notepad安装 1、首先下载Notepad.exe 2、选择简体中文安装 点击下一步 点击“我接受” 选择安装目录&#xff0c;进行下一步安装 默认下一步 选择安装 等待安装完成 点击完成 2、保存json文件 复制返回结果 先把返回结果复制出来。保存到text里面 把文件另存为json格式 3、…

代码随想录-Day44

322. 零钱兑换 给你一个整数数组 coins &#xff0c;表示不同面额的硬币&#xff1b;以及一个整数 amount &#xff0c;表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额&#xff0c;返回 -1 。 你可以认为每种硬币的数…

Qt:8.QWidget属性介绍(focuspolicy属性-控件焦点、stylesheet属性-为控件设置样式)

目录 一、focuspolicy属性-控件焦点&#xff1a; 1.1focuspolicy属性介绍&#xff1a; 1.2设置焦点策略——setFocusPolicy()&#xff1a; 1.3获取控件的焦点策略——focusPolicy()&#xff1a; 二、stylesheet属性——为控件设置样式&#xff1a; 2.1 stylesheet属性介绍…

虚拟机网络配置(静态网络)

解决问题&#xff1a;VMware中创建centOS虚拟机后使用ifconfig没有ip地址&#xff0c;但我想在主机&#xff08;Windows&#xff09;系统下使用shell连接虚拟机从而方便后续交互。 VMware中编辑->虚拟网络编辑器 &#xff08;注意需要管理员身份不然会无法修改&#xff09;…

Python容器 之 列表--定义

1.什么是列表呢&#xff1f; 列表(list)是 Python 中使用最频繁的数据类型, 在其他语言中通常叫做数组, 专门用来存储一组数据 列表,list, 使用 [ ] 列表可以存放任意多个数据 列表中可以存放任意类型的数据 列表中数据之间 使用 逗号隔开 2.列表如何定义&#xff1f; &#…

Android Compose 十二:常用组件列表 上拉加载

列表 上拉加载 当前思路 判断 列表最后一个显示的条目 为 数据集合的长度-1 用来记录刷新状态 var refreshing by remember {mutableStateOf(false)}数据集合 val list remember{List(10){"条目》》${it}"}.toMutableStateList()}用来记录列表当前状态及状态变化…

切片的基础知识

文章目录 ● Slice 的底层实现原理&#xff1f;● array 和 Slice 的区别&#xff1f;● 拷贝大切片一定比小切片代价大吗&#xff1f;● Slice 深拷贝和浅拷贝&#xff1f;● 零切片、空切片、nil切片&#xff1f;● Slice 的扩容机制&#xff1f;● Slice 为什么不是线程安全…

HarmonyOS Next开发学习手册——Native XComponent

场景介绍 Native XComponent是XComponent组件提供在Native层的实例&#xff0c;可作为JS层和Native层XComponent绑定的桥梁。XComponent所提供的NDK接口都依赖于该实例。接口能力包括获取Native Window实例、获取XComponent的布局/事件信息、注册XComponent的生命周期回调、注…

理性决策的艺术:从购房到择偶的数学智慧;37% 规则,做出最佳决策的秘诀;用数学模型解决人生难题

在面对人生重大决策时&#xff0c;如购房或择偶&#xff0c;我们常常感到迷茫和困惑。然而&#xff0c;如果我们能够将这些看似复杂的问题简化为数学模型&#xff0c;我们就能以更加理性和系统的方式做出决策。 37%规则 1950年代&#xff0c;当时几位数学家开始研究这样一个问…

钉钉开放AI生态战略的真正价值到底是什么?很多人都没看懂

来源&#xff1a; 首席数智官 hello 大家好&#xff0c;我们是数字化领军者都在看的首席数智官。 关注我&#xff0c;每天给你讲一个商业案例。 今天我们要给你讲的是&#xff1a;钉钉开放AI大模型生态的战略意义到底是什么&#xff1f; 「谁先赢得苹果&#xff0c;谁就赢得…

C++实现简化版Qt的QObject(3):增加父子关系、属性系统

前几天写了文章&#xff1a; C实现一个简单的Qt信号槽机制 C实现简化版Qt信号槽机制&#xff08;2&#xff09;&#xff1a;增加内存安全保障 之后感觉还不够过瘾&#xff0c;Qt中的QObject体系里还有不少功能特性没有实现。为了提高QObject的还原度&#xff0c;今天我们将父子…

欢迎回家!揭秘“嫦娥六号”背后的守望者

6月25日&#xff0c;嫦娥六号返回器携带来自月背的月球样品安全着陆在内蒙古四子王旗预定区域。这是时隔3年多后&#xff0c;中国探月工程的又一关键节点任务&#xff0c;也是时隔5年多后&#xff0c;嫦娥探测器再去月球背面。 在此次任务中&#xff0c;同元软控数字伴飞团队为…

C++使用Poco库封装一个HTTP客户端类

0x00 前言 我们在使用HTTP协议获取接口数据时&#xff0c;通常需要在Header和Query中添加参数&#xff0c;还有一种就是在Body中追加XML或者JSON格式的数据。本文主要讲述使用Poco库提交HTTP Post请求的Body中附加XML格式的数据&#xff0c;JSON格式的数据类似。 0x01 HttpCl…

Zuul介绍

Zuul 是 Netflix 开源的一个云平台网络层代理&#xff0c;它主要用于路由、负载均衡、中间件通信和动态路由。Zuul 本质上是一个基于 JVM 的网关&#xff0c;它提供了以下功能&#xff1a; 1.路由&#xff1a;Zuul 允许客户端和服务器之间的所有入站和出站请求通过一个中心化的…