java 实现图片新增水印(动态计算水印背景 + 水印文字),附带文字乱码解决方案

文章目录

    • 概要
    • 实现流程
    • 代码如下
    • 小结

概要

图片增加水印背景以及水印文字,根据文字内容是否换行,以及文字行高大小自适应计算背景大小
结果图如下:
实现效果图

实现流程

  1. 定义图片来源,以及读取字体来源(防止中文乱码)
  2. 计算文字所需高度 + 与背景的边距为背景高度
  3. 打印文字

代码如下

public static void main(String[] args) {
        imageWatermark("流淌着阳关的小溪", "深圳市黄浦江", "2024.02.26", "哆啦A梦");
    }

    // 读取项目字体路径
    static final String FONT_PATH = "C:/Users/didadiandi/Desktop/font/simsun.ttc";

    /**
     * 图片水印
     *
     * @date 2024/2/26 15:15
     */
    public static void imageWatermark(String name, String location, String date, String userName) {
        try {
            boolean multiLine = false;
            String locationData = String.format("地    点:%s", location);
            String dateData = String.format("拍摄时间:%s", date);
            String userNameData = String.format("拍摄人员:%s", userName);
            File inputFile = new File("C:/Users/didadiandi/Desktop/微信截图_20240303165815.png");
            BufferedImage inputImage = ImageIO.read(inputFile);

            Graphics2D g2d = (Graphics2D) inputImage.getGraphics();
            // 背景距离下一行数据的距离
            int bgBottomY = inputImage.getWidth() > inputImage.getHeight() ? inputImage.getHeight() / 30 : inputImage.getWidth() / 30;
            // 文字1
            int fontSize01 = inputImage.getWidth() / 42;
            // 文字2
            int fontSize02 = fontSize01 - 2;
            // 文字3
            int fontSize03 = fontSize01 / 2;
            // 时间距离下一行数据的距离
            int dataBottomY = bgBottomY - (bgBottomY / 2);
            // 背景2左边距离
            int bgLeftX02 = bgBottomY + (bgBottomY / 2);

            // 背景
            // 背景2定义
            // 颜色定义
            Color color2 = new Color(255, 255, 255, 111);
            g2d.setColor(color2);
            // 字体文件读取
            Font locationFont = loadStyleFont(FONT_PATH, Font.BOLD, fontSize02);
            g2d.setFont(locationFont);
            // 背景宽度计算
            int bgWatermarkWidth = (inputImage.getWidth() / 3) + (inputImage.getWidth() / 20);
            // 背景X坐标定义
            int bg2X = bgBottomY;
            // 水印文字换行处理
            // 一个文字所占的高度
            int lineHeight02 = g2d.getFontMetrics().getHeight();
            // 每一行的宽度
            int lineWidthLimit2 = bgWatermarkWidth - (dataBottomY * 2);
            // 加上时间以及时间与地点的间隔,总高度累计
            int locationHeightSum = lineHeight02;
            // 换行后地址的X坐标
            int locationXLen = bgLeftX02;
            // 计算任务目标名称的文字换行的总高度
            StringBuilder sbBg2 = new StringBuilder();
            for (char c : locationData.toCharArray()) {
                sbBg2.append(c);
                int locationLen = g2d.getFontMetrics().stringWidth(sbBg2.toString());
                // 一行的宽度 是否大于规定的行宽度
                if (locationLen + locationXLen > lineWidthLimit2) {
                    locationXLen = fontSize02 * 6;
                    locationHeightSum += lineHeight02;
                    sbBg2 = new StringBuilder();
                }
            }
            // 时间所需高度
            int dateHeight = lineHeight02 + dataBottomY;
            // 地址所需高度
            locationHeightSum = locationHeightSum + (dataBottomY * 2);
            // 图形的大小等于文字的换行总长度 + 间距
            int bg2WatermarkHeight = locationHeightSum + dateHeight + (lineHeight02 + dataBottomY);
            // 背景X坐标定义
            int bg1X = bgBottomY;
            // 背景图片的坐标
            int bg2Y = inputImage.getHeight() - (bg2WatermarkHeight + bgBottomY);
            // 将图形定义到图片上
            g2d.fillRect(bg2X, bg2Y, bgWatermarkWidth, bg2WatermarkHeight);
            // 背景1
            // 背景1颜色定义
            Color color1 = new Color(203, 82, 82, 150);
            g2d.setColor(color1);
            // 背景1上文字的定义
            Font nameFont = loadStyleFont(FONT_PATH, Font.BOLD, fontSize01);
            g2d.setFont(nameFont);
            // 水印文字换行处理
            // 一个文字所占的高度
            int lineHeight = g2d.getFontMetrics().getHeight();
            // 每一行的宽度
            int lineWidthLimit1 = bgWatermarkWidth - (dataBottomY * 2) - (bgLeftX02 * 2) + fontSize03;
            int nameHeight = lineHeight;
            // 计算任务目标名称的文字换行的总高度
            StringBuilder sbBg1 = new StringBuilder();
            for (char c : name.toCharArray()) {
                sbBg1.append(c);
                // 一行的宽度 是否大于规定的行宽度
                if (g2d.getFontMetrics().stringWidth(sbBg1.toString()) > lineWidthLimit1) {
                    nameHeight += lineHeight;
                    sbBg1 = new StringBuilder();
                }
            }
            // 图形的大小等于文字的换行总长度 + 间距
            int bg1WatermarkHeight = nameHeight + dataBottomY + (fontSize01 / 2);
            // 背景图片的坐标
            int bg1Y = bg2Y - (bg1WatermarkHeight);
            g2d.fillRect(bg1X, bg1Y, bgWatermarkWidth, bg1WatermarkHeight);

            // 文字

            // 字体定义
            Font font3 = loadStyleFont(FONT_PATH, Font.BOLD, fontSize01);
            g2d.setFont(font3);
            // 颜色定义
            Color color3 = new Color(231, 243, 243, 255);
            g2d.setColor(color3);
            // 任务目标名称的水印文字换行处理
            // 获取名称当前字体行高
            int nameLineHeight = g2d.getFontMetrics().getHeight();
            // 稳字行高累计
            int nameSumLineHeight = nameLineHeight;
            // 名字X坐标为 两倍bgLeftX02 边距 + 稳字所需大小
            int nameX = (bgLeftX02 * 2) + fontSize03;
            // 名字Y坐标为,在背景Y坐标的基础上向下移动 数据的Y坐标边距 + 文字1大小的边距
            int nameY = bg1Y + (dataBottomY + fontSize01);
            // 名字实际Y坐标(换行所必须参数)
            int nameY2 = nameY;
            StringBuilder nameSb = new StringBuilder();
            for (char c : name.toCharArray()) {
                nameSb.append(c);
                if (g2d.getFontMetrics().stringWidth(nameSb.toString()) > lineWidthLimit1 - dataBottomY) {
                    // 将文字写入图片
                    g2d.drawString(nameSb.toString(), nameX, nameY2);
                    // 名字下次换行所需Y坐标
                    nameY2 += nameLineHeight;
                    // 名字总高度
                    nameSumLineHeight += nameLineHeight;
                    nameSb = new StringBuilder();
                    // 是否换行
                    multiLine = true;
                }
            }
            g2d.drawString(nameSb.toString(), nameX, nameY2);

            // 点坐标定义
            Font font5 = loadStyleFont(FONT_PATH, Font.BOLD, fontSize03);
            g2d.setFont(font5);
            Color color5 = new Color(0, 0, 0, 255);
            g2d.setColor(color5);
            String point = "●";
            int pointX = bgLeftX02;
            // 点坐标等于初始文字坐标,如果有多行,则是多行的二分之一坐标
            int pointY = multiLine ? nameY + (nameSumLineHeight / 2) : nameY;
            g2d.drawString(point, pointX, pointY);

            // 时间定义
            Font font4 = loadStyleFont(FONT_PATH, Font.BOLD, fontSize02);
            g2d.setFont(font4);
            Color color4 = new Color(35, 40, 36, 255);
            g2d.setColor(color4);
            // 水印文字换行处理
            int fontSize02LineHeight = g2d.getFontMetrics().getHeight();

            int dateX = bgLeftX02;
            int dateY = bg2Y + (dataBottomY + fontSize02);
            g2d.drawString(dateData, dateX, dateY);

            // 人员定义
            int userNameX = bgLeftX02;
            int userNameY = dateY + (dataBottomY + fontSize02);
            g2d.drawString(userNameData, userNameX, userNameY);

            // 地址
            int locationX = bgLeftX02;

            int locationY = userNameY + dataBottomY + fontSize02LineHeight;
            StringBuilder locationSb = new StringBuilder();
            for (char c : locationData.toCharArray()) {
                locationSb.append(c);
                int locationLen = g2d.getFontMetrics().stringWidth(locationSb.toString());
                if (locationLen + locationX > lineWidthLimit2 - dataBottomY) {
                    g2d.drawString(locationSb.toString(), locationX, locationY);
                    locationY += fontSize02LineHeight;
                    locationX = fontSize02 * 6;
                    locationSb = new StringBuilder();
                }
            }
            g2d.drawString(locationSb.toString(), locationX, locationY);
            // 回收资源
            g2d.dispose();

            File outputFile = new File("output.png");
            ImageIO.write(inputImage, "png", outputFile);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @Created by <a href="https://blog.csdn.net/weixin_44951037/article/details/109623821">马男波杰克</a>
     *
     * @param fontFileName 外部字体名
     * @param style        字体样式
     * @param fontSize     字体大小
     * @return
     */
    public static Font loadStyleFont(String fontFileName, int style, float fontSize) throws IOException, FontFormatException {
        try (InputStream in = ResourceUtil.getStream(fontFileName);){
            Font dynamicFont = Font.createFont(Font.TRUETYPE_FONT, in);
            return dynamicFont.deriveFont(style, fontSize);
        }
    }

小结

  1. 背景的高度是文字高度 + 文字与背景所需边距
  2. x坐标都是一致的
  3. 图片第一个文字水印Y坐标定义好之后,其他坐标都可以在这个Y坐标的基础上进行计算,可以保证水印的整体性(即调整前面一个元素的Y坐标,后边元素的Y坐标同时变动)

文字乱码解决方式参考:马男波杰克,读取字体文件解决乱码

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

云上攻防-云原生篇KubernetesK8s安全APIKubelet未授权访问容器执行

知识点 1、云原生-K8s安全-名词架构&各攻击点 2、云原生-K8s安全-Kubelet未授权访问 3、云原生-K8s安全-API Server未授权访问 章节点&#xff1a; 云场景攻防&#xff1a;公有云&#xff0c;私有云&#xff0c;混合云&#xff0c;虚拟化集群&#xff0c;云桌面等 云厂商…

PyInstaller 打包 Python 应用程序一键双击运行

PyInstaller 打包 Python 应用程序一键双击运行 作为一个词语&#xff0c;“活着”在语言里充满了力量&#xff0c;它的力量不是来自于喊叫&#xff0c;也不是来自于进攻&#xff0c;而是忍受&#xff0c;去忍受生命赋予我们的责任&#xff0c;去忍受现实给予我们的幸福和苦难、…

独立游戏《星尘异变》UE5 C++程序开发日志2——创建并编写一个C++类

在本篇日志中&#xff0c;我们将要用一个C类来实现一个游戏内的物品&#xff0c;同时介绍UCLASS、USTRUCT、UPROPERTY的使用 一、创建一个C类 我们在UE5的"内容侧滑菜单"中&#xff0c;在右侧空白中右键选择"新建C类"&#xff0c;然后可以选择一个想要的…

基础二分学习笔记

模板 : 个人倾向第一种 ; 整数二分 : 最大化查找 : 可行区域在左侧 : 查找最后一个<q的数的下标 : int find(int q){// 查找最后一个 < q 的下标 int l 0 , r n 1 ;while(l 1 < r){int mid l r >> 1 ;if(a[mid]<q) l mid ;else r mid ;}return…

【设计模式 01】单例模式

单例模式&#xff0c;是一种创建型设计模式&#xff0c;他的核心思想是保证一个类只有一个实例&#xff08;即&#xff0c;在整个应用程序中&#xff0c;只存在该类的一个实例对象&#xff0c;而不是创建多个相同类型的对象&#xff09;&#xff0c;并提供一个全局访问点来访问…

深入了解 JavaScript 混淆加密和环境检测

JavaScript混淆加密是一种通过修改代码结构和命名约定来增加代码的复杂性&#xff0c;使其难以被理解和逆向工程的技术。在这篇文章中&#xff0c;我们将深入探讨JS混淆加密的一些逻辑&#xff0c;并介绍如何通过环境检测来提高代码的安全性。我们将使用案例代码演示这些概念。…

微信小程序开发学习笔记《18》uni-app框架-网络请求与轮播图

微信小程序开发学习笔记《18》uni-app框架-网络请求 博主正在学习微信小程序开发&#xff0c;希望记录自己学习过程同时与广大网友共同学习讨论。建议仔细阅读uni-app对应官方文档 一、下载网络请求包 这个包是以前黑马程序员老师写的一个包&#xff0c;跟着课程学习&#x…

LSTM 长短期记忆递归神经网络

1、神经网络简介 1.1 神经网络起源 人工神经网络&#xff08;Aritificial Neural Networks, ANN&#xff09;是一种仿生的网络结构&#xff0c;起源于对人类大脑的研究。人工神经网络&#xff08;Aritificial Neural Networks&#xff09;也常被简称为神经网络&#xff08;Ne…

考研复试指南

1. 记住&#xff0c;复试的本质不是考试&#xff0c;而是一场自我展示。 考研复试并非简单的知识考察&#xff0c;更是一场展示自我能力和潜力的机会。除了学科知识&#xff0c;考官更关注你的综合素质、学术兴趣和未来发展规划。因此&#xff0c;要保持自信&#xff0c;用更全…

重读 Java 设计模式: 探索经典之道与 Spring 框架的设计

写在开头 记得大学刚毕业那会儿&#xff0c;想学点东西&#xff0c;于是拿出了《Head First 设计模式》这本书&#xff0c;就开始了阅读&#xff0c;我曾对这些模式感到晦涩难懂。然而&#xff0c;随着工作岁月的增长&#xff0c;我逐渐领悟到设计模式的价值&#xff0c;尤其是…

使用 Haproxy 搭建Web群集

Haproxy是目前比较流行的一种群集调度工具&#xff0c;同类群集调度工具有很多&#xff0c;如LVS 和Nginx。相比较而言&#xff0c;LVS.牲能最好&#xff0e;但是搭建相对复杂:Nginx的upstream模块支持群集功能&#xff0e;但是对群集节点健康检查功能不强&#xff0c;性能没有…

jupyter 一键快捷启动方法研究

1.效果 首先打开dat 文件&#xff0c;同意赋予管理员 输入序号1 成功启动 2.Bat代码 %1 mshta vbscript:CreateObject("Shell.Application").ShellExecute("cmd.exe","/c %~s0 ::","","runas",1)(window.close)&&e…

【网站项目】123网上书城系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

Matlab 多项式插值(曲线拟合)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 由于对曲线拟合有些兴趣,这里就找了一些资料从最基本的方法来看一下曲线拟合的效果: 二、实现代码 % **********

后端开发技术面试指南

工作10多年&#xff0c;每年都会帮组里面试一些新同学校招社招的都有&#xff0c;下面我就从一个面试官的视角来给大家拆解一下如何淡然应对后端开发技术面试。 1.一面多为电话面试 (1)问七问八 ①简历要注重内容&#xff0c;形式上不丑没有错别字即可。之前收到过一个工作5…

代码随想录算法训练营第七天

● 自己看到题目的第一想法 第454题.四数相加II 方法&#xff1a; 方法一&#xff1a; 暴力法 思路&#xff1a; 注意&#xff1a; 代码&#xff1a; class Solution { public:int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<i…

SpringBlade CVE-2022-27360 export-user SQL 注入漏洞分析

漏洞描述 SpringBlade是一个基于Spring Cloud和Spring Boot的开发框架&#xff0c;旨在简化和加速微服务架构的开发过程。它提供了一系列开箱即用的功能和组件&#xff0c;帮助开发人员快速构建高效可靠的微服务应用。该产品/api/blade-user/export-user接口存在SQL注入。 漏…

探索Hadoop的三种运行模式:单机模式、伪分布式模式和完全分布式模式

目录 前言一、 单机模式二、 伪分布式模式三、 完全分布式模式&#xff08;重点&#xff09;3.1 准备工作3.2 配置集群3.2.1 配置core-site.xml 文件3.2.2 配置hdfs-site.xml 文件3.2.3 配置yarn-site.xml 文件3.2.4 配置mapred-site.xml 文件 3.3 启动集群3.3.1 配置workers3.…

神经网络系列---卷积

文章目录 卷积神经网络卷积转置卷积 卷积核和反卷积的三种实现方式卷积的次数计算 卷积神经网络 在神经网络的卷积层中&#xff0c;向下取整&#xff08;Floor&#xff09;是一种常用的策略&#xff0c;特别是在处理输出尺寸不是整数的情况时。当你计算出卷积层输出的尺寸&…

【 10X summary report】怎么看?详细解读笔记

报告内容 在开始正式的分析之前&#xff0c;需要查看在对齐和计数过程中生成的任何总结统计信息。下图是由Cell Ranger工具创建的10X总结报告&#xff0c;在从10X scRNA-seq实验生成计数矩阵时会生成。 The left half of the report describes sequencing and mapping statist…