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

循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...

CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...

Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...
BLEU评分:机器翻译质量评估的黄金标准
BLEU评分:机器翻译质量评估的黄金标准 1. 引言 在自然语言处理(NLP)领域,衡量一个机器翻译模型的性能至关重要。BLEU (Bilingual Evaluation Understudy) 作为一种自动化评估指标,自2002年由IBM的Kishore Papineni等人提出以来,…...
uniapp 集成腾讯云 IM 富媒体消息(地理位置/文件)
UniApp 集成腾讯云 IM 富媒体消息全攻略(地理位置/文件) 一、功能实现原理 腾讯云 IM 通过 消息扩展机制 支持富媒体类型,核心实现方式: 标准消息类型:直接使用 SDK 内置类型(文件、图片等)自…...

使用SSE解决获取状态不一致问题
使用SSE解决获取状态不一致问题 1. 问题描述2. SSE介绍2.1 SSE 的工作原理2.2 SSE 的事件格式规范2.3 SSE与其他技术对比2.4 SSE 的优缺点 3. 实战代码 1. 问题描述 目前做的一个功能是上传多个文件,这个上传文件是整体功能的一部分,文件在上传的过程中…...

Windows电脑能装鸿蒙吗_Windows电脑体验鸿蒙电脑操作系统教程
鸿蒙电脑版操作系统来了,很多小伙伴想体验鸿蒙电脑版操作系统,可惜,鸿蒙系统并不支持你正在使用的传统的电脑来安装。不过可以通过可以使用华为官方提供的虚拟机,来体验大家心心念念的鸿蒙系统啦!注意:虚拟…...