Java-如何使用Java将图片和文字拼接在一起(并非是给图片加水印)
之前有遇到一个问题
问题背景:项目中,有一个功能,管理端可以将客户创建的小程序码下载到本地,方便客户将对应门店的小程序码打印出来并张贴到门店,做门店的引流和会员入会。
具体问题:当小程序码的数量较少的时候,我们是后端将小程序码的分组信息和小程序码的图片以树的数据结构形式,返回给前端,由前端拿到分组信息和小程序图片链接,在前端进行下载小程序码图片,并将分组信息拼接在小程序码的下方,类似这样:

但是,当这个门店的结构复杂之后,小程序码的数量也多了起来,由前端来下载就显得非常让人焦灼了,前端只能使用下载的这台电脑的性能来一张一张的下载小程序码并拼接门店的信息,1000多张小程序码的话,就需要10分钟左右的等待时间,有的客户的电脑性能比较差的话,干脆就没办法下载,怎么办呢?
一句话,放后端并行下载呗,然后直接返回zip包的流数据文件给前端不就行了。。。
当时接到这个任务,我也天真的认为,搞个线程并行下载,然后打包不就OK了么,能有多费事呢?服务器随随便便不就16核+64G的配置,下载个文件就算网络差点,千把个图片还不是分分钟的事儿嘛,领导面前胸口拍得梆梆响,小事一桩嘛~~~
下载倒是好说,并发 CountDownLatch cdl = new CountDownLatch(size); 控制下下载的次序下载完再一起打包。。。
但是,把文字怎么搞到这张小程序码图片的下面呢,又不能拉伸这张图片,那就要把文字先转成图片(跟小程序码图片宽度保持一致)
a.先把远程的图片下载到本地
String localFilePath = "D:\\Download\\0402" + File.separator;
String localFileName = "test3.png";
downloadFile("http://0.0.0.0:8080/photo/gh_ff959c80f0d7_1280.jpg", localFilePath, localFileName);/*** 下载远程文件并保存到本地*/
public static void downloadFile(String remoteFilePath, String localFilePath, String fileName) {FileUtil.mkdir(localFilePath);ReadableByteChannel rbc = null;FileOutputStream fos = null;try {URL website = new URL(remoteFilePath);rbc = Channels.newChannel(website.openStream());fos = new FileOutputStream(localFilePath + fileName);fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);} catch (Exception e) {log.warn(e.getMessage());} finally {IOUtils.closeQuietly(fos);IOUtils.closeQuietly(rbc);}}
b.将第一行文字和第二行文字转换成跟小程序码图片一样宽度的图片
/*** 小程序码 导出组名 二维码名 字体*/public static final String SOURCE_HAN_SANS = "思源黑体 CN Regular";/*** 小程序码 导出 二维码名 字号*/public static final int FONT_SIZE_TITLE = 24;/*** 小程序码 导出组名 字号*/public static final int FONT_SIZE_GROUP_NAME = 14;/*** 小程序码 导出二维码图片 缩略图 宽度*/public static final int MA_QR_CODE_IMAGE_WIDTH = 552;/*** 小程序码 导出二维码图片 缩略图 高度*/public static final int MA_QR_CODE_IMAGE_HEIGHT = 552;/*** 小程序码 文字生成图片 背景高度*/public static final int BACK_GROUND_IMAGE_HEIGHT = 35;/*** 小程序码 导出二维码名 每行长度*/public static final int MA_QR_CODE_SPLIT_SIZE = 21;/*** 小程序码 导出组名 每行长度*/public static final int MA_GROUP_SPLIT_SIZE = 38;//第一行文字
String fileName = "ZSHMD上海市浦东新区东方体育中心万达购物中心店";
String targetFile = localFilePath + "test3_t1.png";
TextToImage.textToImage(TextToImage.ImageContent.buildOf(MA_QR_CODE_IMAGE_WIDTH, MA_QR_CODE_SPLIT_SIZE, Color.BLACK, SOURCE_HAN_SANS, FONT_SIZE_TITLE, fileName, targetFile));
//第二行文字
String groupFullName = "全部-中国-上海-浦东新区-三林镇";
String groupTargetFile = localFilePath + "test3_t2.png";
TextToImage.textToImage(TextToImage.ImageContent.buildOf(MA_QR_CODE_IMAGE_WIDTH, MA_GROUP_SPLIT_SIZE, Color.GRAY, SOURCE_HAN_SANS, FONT_SIZE_GROUP_NAME, groupFullName, groupTargetFile));
工具类方法:
/*** 将文字转换为png图片*/public static void textToImage(ImageContent content) throws IOException {//小程序码 文字生成图片 背景高度String contentText = content.getText();if (StringUtils.isEmpty(contentText)) {return;}String[] texts = contentText.split("(?<=\\G.{" + content.getSplitSize() + "})");int height = texts.length * BACK_GROUND_IMAGE_HEIGHT;//创建图片int width = content.getWidth();BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);Graphics2D graphics = bufferedImage.createGraphics();//设置背景graphics.fillRect(0, 0, width, height);//定义字体Font font = new Font(content.getFontName(), Font.PLAIN, content.getFontSize());// 防止生成的文字带有锯齿graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);//设置颜色graphics.setColor(content.getColor());//设置字体graphics.setFont(font);//写入 (getWordWidth 计算该字体文本的长度) 居中Lists.mutable.of(texts).forEachWithIndex((x, i) ->graphics.drawString(x, (width - getWordWidth(font, x)) / 2, 20 + 30 * i));graphics.dispose();ImageIO.write(bufferedImage, PNG, new File(content.getTargetFile()));}
工具类-内部类:
@Datapublic static class ImageContent{private int width;private int splitSize;private Color color;private String fontName;private int fontSize;private String text;private String targetFile;public static ImageContent buildOf(int maQrCodeImageWidth, int maQrCodeSplitSize, Color color, String fontName, int fontSize, String text, String targetFile) {ImageContent content = new ImageContent();content.setWidth(maQrCodeImageWidth);content.setSplitSize(maQrCodeSplitSize);content.setColor(color);content.setFontName(fontName);content.setFontSize(fontSize);content.setText(text);content.setTargetFile(targetFile);return content;}}@Getter@AllArgsConstructorpublic enum SpliceType{/** */TRANSVERSE("横向"),PORTRAIT("纵向");private final String desc;}
c.将小程序码图片生成552x552大小的缩略图
//生成图片的缩略图 552x552
Thumbnails.of(localFilePath + localFileName).size(MA_QR_CODE_IMAGE_WIDTH, MA_QR_CODE_IMAGE_HEIGHT).keepAspectRatio(false).toFile(localFilePath + "test3_t0.png");maven依赖:
<!-- https://mvnrepository.com/artifact/net.coobird/thumbnailator -->
<dependency><groupId>net.coobird</groupId><artifactId>thumbnailator</artifactId><version>0.4.8</version>
</dependency>
d.拼接图片,生成想要的那种图片结构
TextToImage.mergeImage(new String[]{localFilePath + "test3_t0.png", localFilePath + "test3_t1.png", localFilePath + "test3_t2.png"}, TextToImage.SpliceType.PORTRAIT, localFilePath + File.separator + "test3.png");/*** @param files 要拼接的图片列表* @param type 1 横向拼接, 2 纵向拼接* 图片拼接 (注意:必须两张图片长宽一致)*/public static void mergeImage(String[] files, SpliceType type, String targetFile) {int len = files.length;if (len < 1) {log.warn("图片数量小于1");return;}File[] src = new File[len];BufferedImage[] images = new BufferedImage[len];int[][] imageArrays = new int[len][];for (int i = 0; i < len; i++) {try {src[i] = new File(files[i]);images[i] = ImageIO.read(src[i]);} catch (Exception e) {log.warn("{}", e.getMessage(), e);}int width = images[i].getWidth();int height = images[i].getHeight();imageArrays[i] = new int[width * height];imageArrays[i] = images[i].getRGB(0, 0, width, height, imageArrays[i], 0, width);}int newHeight = 0;int newWidth = 0;for (BufferedImage image : images) {// 横向if (SpliceType.TRANSVERSE == type) {newHeight = Math.max(newHeight, image.getHeight());newWidth += image.getWidth();}// 纵向if (SpliceType.PORTRAIT == type) {newWidth = Math.max(newWidth, image.getWidth());newHeight += image.getHeight();}}if (SpliceType.TRANSVERSE == type && newWidth < 1) {return;}if (SpliceType.PORTRAIT == type && newHeight < 1) {return;}// 生成新图片try {BufferedImage imageNew = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);int height = 0;int width = 0;for (int i = 0; i < images.length; i++) {if (SpliceType.TRANSVERSE == type) {imageNew.setRGB(width, 0, images[i].getWidth(), newHeight, imageArrays[i], 0, images[i].getWidth());width += images[i].getWidth();}if (SpliceType.PORTRAIT == type) {imageNew.setRGB(0, height, newWidth, images[i].getHeight(), imageArrays[i], 0, newWidth);height += images[i].getHeight();}}//输出想要的图片ImageIO.write(imageNew, PNG, new File(targetFile));} catch (Exception e) {log.warn("{}", e.getMessage(), e);}}
注意,如果部署Linux上之后,可能会发现文字的位置是空的,那就需要在项目根目录下安装一下字体哦。


//始终都删除中间生成的临时文件
Lists.mutable.of("test3_t0.png", "test3_t1.png", "test3_t2.png").forEach(FileUtil::del);
好了,到此,这个问题已经解决了,并不知道有大佬有没有更好的办法呢,还请不吝赐教呀~
相关文章:
Java-如何使用Java将图片和文字拼接在一起(并非是给图片加水印)
之前有遇到一个问题 问题背景:项目中,有一个功能,管理端可以将客户创建的小程序码下载到本地,方便客户将对应门店的小程序码打印出来并张贴到门店,做门店的引流和会员入会。 具体问题:当小程序码的数量较少…...
Metasploit入门到高级【第三章】
来自公粽号:Kali与编程预计更新第一章:Metasploit 简介 Metasploit 是什么Metasploit 的历史和发展Metasploit 的组成部分 第二章:Kali Linux 入门 Kali Linux 简介Kali Linux 安装和配置常用命令和工具介绍 第三章:Metasploi…...
枚举的使用
Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等。1 问题如何在类中使用枚举,例如枚举出一年的四个季度,并且通过迭代枚举…...
Python进阶语法
1.1 Python进阶语法 1.1.1 交换变量 一行代码快速交换两个变量,无需创建临时变量。 from icecream import ica 2 b 4 a, b b, a ic(a, b)ic| a: 4, b: 2 1.1.2 链式比较 from icecream import ica 97 if 90 < a < 100:ic(a)ic| a: 97 1.1.3 初始化列表…...
Pyspark_结构化流4
Pyspark 注:大家觉得博客好的话,别忘了点赞收藏呀,本人每周都会更新关于人工智能和大数据相关的内容,内容多为原创,Python Java Scala SQL 代码,CV NLP 推荐系统等,Spark Flink Kafka Hbase Hi…...
Linux cmp 命令
Linux cmp 命令用于比较两个文件是否有差异。 当相互比较的两个文件完全一样时,则该指令不会显示任何信息。若发现有所差异,预设会标示出第一个不同之处的字符和列数编号。若不指定任何文件名称或是所给予的文件名为"-",则cmp指令…...
Python入门到高级【第五章】
预计更新第一章. Python 简介 Python 简介和历史Python 特点和优势安装 Python 第二章. 变量和数据类型 变量和标识符基本数据类型:数字、字符串、布尔值等字符串操作列表、元组和字典 第三章. 控制语句和函数 分支结构:if/else 语句循环结构&#…...
C语言中(i++)+ (i++)真的每次都等于3吗?
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言结论证明首先,登场的是我们的VC6.0(还有Linux)最后一位,我使用了小熊猫C(还有Clion)请添加…...
Cursor,程序员的 AI 代码编辑助手
相信大家都或多或少地听说过、了解过 chatGPT ,半个月前发布的 GPT-4 ,可谓是 AI 赛道上的一个王炸 那么今天咸鱼给大家分享一个开源的 AI 代码编辑器——Cursor,让各位程序员在编程之路上一骑绝尘 😃 介绍 Cursor 是一个人工智…...
基于XML的自动装配~
基于XML的自动装配之场景模拟: 自动装配:根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或者接口类型赋值 之前我们学过的依赖注入,我们在为不同属性赋值时,例如类类型的属性…...
完全二叉树的4种遍历方式
一张二叉树的图 1,二叉树的特点 每个点p的左儿子是p*2,右儿子是p*21,可以分别表示为p<<1与p<<1|1节点的序号是从左到右,从上到下增加的每个点至多2个儿子(屁话(bushi)) 2ÿ…...
【vue2】使用elementUI进行表单验证实操(附源码)
🥳博 主:初映CY的前说(前端领域) 🌞个人信条:想要变成得到,中间还有做到! 🤘本文核心:vue使用elementUI进行表单验证实操(附源码) 【前言】我们在构建一…...
JUC之阻塞队列解读(BlockingQueue)
目录 BlockingQueue 简介 BlockingQueue 核心方法 1.放入数据 2.获取数据 入门代码案例 常见的 BlockingQueue ArrayBlockingQueue(常用) LinkedBlockingQueue(常用) PriorityBlockingQueue SynchronousQueue LinkedTransferQueue LinkedBlockingDeque 小结 Bloc…...
LCHub:ChatGPT4和低代码来临,程序员面临下岗?
一个网友吐槽道: “ 建站出来了,你们说程序员会失业。 低代码出来了,你们说程序员会失业。 Copilot出来了,你们说程序员会失业。 Chatgpt出来了,你们说程序员会失业 虽然这只是网友的吐槽,但却引起了小编的好奇。为何程序员那么容易被新技术取代?今天小编打算跟大家…...
【Node.js】Express框架的基本使用
✍️ 作者简介: 前端新手学习中。 💂 作者主页: 作者主页查看更多前端教学 🎓 专栏分享:css重难点教学 Node.js教学 从头开始学习 目录 初识Express Express简介 什么是Express 进一步理解 Express Express能做什么 Express的基本使用 …...
使用docker 和 kubnernetes 部署单节点/多节点 kafka 环境
参考资料 https://kafka.apachecn.org/documentation.html#configuration kafka的broker有三个核心配置 broker.idlog.dirszookeeper.connect docker启动单节点kafka环境 启动zookeeper 可配置的环境变量,https://gallery.ecr.aws/bitnami/zookeeper $ docker …...
Linux使用:环境变量指南和CPU和GPU利用情况查看
Linux使用:环境变量指南和CPU和GPU利用情况查看Linux环境变量初始化与对应文件的生效顺序Linux的变量种类设置环境变量直接运行export命令定义变量修改系统环境变量修改用户环境变量修改环境变量配置文件环境配置文件的区别profile、 bashrc、.bash_profile、 .bash…...
深入浅出 SSL/CA 证书及其相关证书文件(pem、crt、cer、key、csr)
互联网是虚拟的,通过互联网我们无法正确获取对方真实身份。数字证书是网络世界中的身份证,数字证书为实现双方安全通信提供了电子认证。数字证书中含有密钥对所有者的识别信息,通过验证识别信息的真伪实现对证书持有者身份的认证。数字证书可…...
Compose(1/N) - 概念 基本使用
一、概念 1.1 解决的问题 APP展示的数据绝大多数不是静态数据而是会实时更新,传统的命令式UI写法更新界面繁琐且容易同步错误。1.2 Compose优势 由一个个可组合的Composable函数(可看作是一个Layout布局)拼成界面,方便维护和复用…...
2023高质量Java面试题集锦:高级Java工程师面试八股汇总
人人都想进大厂,当然我也不例外。早在春招的时候我就有向某某某大厂投岗了不少简历,可惜了,疫情期间都是远程面试,加上那时自身也有问题,导致屡投屡败。突然也意识到自己肚子里没啥货,问个啥都是卡卡卡卡&a…...
在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:
在 HarmonyOS 应用开发中,手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力,既支持点击、长按、拖拽等基础单一手势的精细控制,也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档,…...
【大模型RAG】Docker 一键部署 Milvus 完整攻略
本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装;只需暴露 19530(gRPC)与 9091(HTTP/WebUI)两个端口,即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...
现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...
2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...
React---day11
14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store: 我们在使用异步的时候理应是要使用中间件的,但是configureStore 已经自动集成了 redux-thunk,注意action里面要返回函数 import { configureS…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
Caliper 配置文件解析:fisco-bcos.json
config.yaml 文件 config.yaml 是 Caliper 的主配置文件,通常包含以下内容: test:name: fisco-bcos-test # 测试名称description: Performance test of FISCO-BCOS # 测试描述workers:type: local # 工作进程类型number: 5 # 工作进程数量monitor:type: - docker- pro…...
【LeetCode】算法详解#6 ---除自身以外数组的乘积
1.题目介绍 给定一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O…...
HybridVLA——让单一LLM同时具备扩散和自回归动作预测能力:训练时既扩散也回归,但推理时则扩散
前言 如上一篇文章《dexcap升级版之DexWild》中的前言部分所说,在叠衣服的过程中,我会带着团队对比各种模型、方法、策略,毕竟针对各个场景始终寻找更优的解决方案,是我个人和我司「七月在线」的职责之一 且个人认为,…...
Docker拉取MySQL后数据库连接失败的解决方案
在使用Docker部署MySQL时,拉取并启动容器后,有时可能会遇到数据库连接失败的问题。这种问题可能由多种原因导致,包括配置错误、网络设置问题、权限问题等。本文将分析可能的原因,并提供解决方案。 一、确认MySQL容器的运行状态 …...
