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…...

深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...

多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...

python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...

大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...

python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...

visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...