Java 图片合成
前序
本周接到了新项目中的一个需求:根据给定的内容合成一张图片,需求如下:
- 标题自动换行,如果标题中出现英文单词时,以单词为最小单元进行换行。
- 如果行数超过5行省略用 … 代替。
- 符号是下一行首字母时,自动截留到上一行末尾。
- 空格为下一行开头,则删除空格,显示单词,保持内容左对齐
技术栈(JAI)
Java Advanced Imaging (JAI) 是一个用于处理图像的开源Java库。它提供了一个框架,可以用来访问各种图像源,包括本地文件系统、网络资源以及数据库等,并可以对这些图像进行转换和分析。
优点:JDK 自带内容,操作简单,不用引入新的依赖。
代码概述
本功能通过加载本地的背景图片和传入的参数进行图片的合成。主要包含了背景图、二维码插图和文本内容。其中二维码插图通过 HuTool
工具包提供生成方法。
具体代码
QrCodeImageConfig.java(二维码插图配置类)
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.extra.qrcode.QrConfig;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
/**
* 插图参数
*/
@Getter
@Slf4j
public class QrCodeImageConfig {
/**
* 二维码内容
*/
private final String text;
/**
* 二维码宽度
*/
private final Integer width;
/**
* 二维码高度
*/
private final Integer height;
/**
* 二维码颜色
*/
private final Color color;
/**
* 二维码图片接收对象
*/
private final File qrCodeImgFile;
/**
* JAI 图片对象
*/
private final BufferedImage qrCodeBuffer;
/**
* 构造函数
*
* @param text 二维码内容
* @param width 二维码宽度
* @param height 二维码高度
* @param color 二维码颜色
* @param qrCodeImg 二维码图片接收对象
*/
public QrCodeImageConfig(String text, Integer width, Integer height, Color color, File qrCodeImg) {
if (StringUtils.isBlank(text) || width == null || height == null || color == null || qrCodeImg == null) {
throw new IllegalArgumentException("QrCodeImageParam.QrCodeImageParam 参数异常");
}
this.color = color;
this.width = width;
this.height = height;
this.text = text;
this.qrCodeImgFile = qrCodeImg;
try {
QrConfig config = new QrConfig(width, height);
config.setBackColor(color);
config.setMargin(1);
QrCodeUtil.generate(text, config, qrCodeImg);
this.qrCodeBuffer = ImageIO.read(qrCodeImg);
} catch (IOException e) {
throw new RuntimeException("QrCodeImageParam.QrCodeImageParam2 二维码图片配置创建异常");
}
}
/**
* 获取图片高度
*/
public int getImageHeight() {
return qrCodeBuffer.getHeight();
}
/**
* 获取图片宽度
*/
public int getImageWidth() {
return qrCodeBuffer.getWidth();
}
}
TextConfig.java(文本内容配置类)
import lombok.Getter;
import java.awt.*;
/**
* 文本内容配置
*/
@Getter
public class TextConfig {
/**
* 文本内容
*/
private final String text;
/**
* 字体库
*/
private final String typeface;
/**
* 是否加粗
*/
private final Boolean boldFont;
/**
* 字号
*/
private final Integer fontSize;
/**
* 字体颜色
*/
private final Color fontColor;
/**
* 行间距
*/
private final Integer lineSpacing;
/**
* 字间距
*/
private final Integer wordSpace;
/**
* x轴坐标
*/
private final Integer x;
/**
* y轴坐标
*/
private Integer y;
/**
* 构造函数
*
* @param text 文本内容
* @param typeface 字体库
* @param boldFont 是否加粗
* @param fontSize 字号
* @param fontColor 字体颜色
* @param lineSpacing 行间距
* @param wordSpace 字间距
* @param x x轴坐标
* @param y y轴坐标
*/
public TextConfig(String text, String typeface, Boolean boldFont, Integer fontSize, Color fontColor,
Integer lineSpacing, Integer wordSpace, Integer x, Integer y) {
this.text = text;
this.typeface = typeface;
this.boldFont = boldFont;
this.fontSize = fontSize;
this.fontColor = fontColor;
this.lineSpacing = lineSpacing;
this.wordSpace = wordSpace;
this.x = x;
this.y = y;
}
/**
* 构造函数
*
* @param text 文本内容
* @param typeface 字体库
* @param boldFont 是否加粗
* @param fontSize 字号
* @param fontColor 字体颜色
* @param lineSpacing 行间距
* @param wordSpace 字间距
* @param x x轴坐标
*/
public TextConfig(String text, String typeface, Boolean boldFont, Integer fontSize, Color fontColor,
Integer lineSpacing, Integer wordSpace, Integer x) {
this.text = text;
this.typeface = typeface;
this.boldFont = boldFont;
this.fontSize = fontSize;
this.fontColor = fontColor;
this.lineSpacing = lineSpacing;
this.wordSpace = wordSpace;
this.x = x;
}
}
GenerateAttendanceImageUtil.java(签到图片生成工具类)
import lombok.extern.slf4j.Slf4j;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* 生成签到图片工具类
*/
@Slf4j
public class GenerateAttendanceImageUtil {
private static int y = 0;
/**
* 生成签到页图片(PNG格式)
*
* @param backgroundImage 背景图片
* @param title 会议主题配置
* @param dateAddress 时间地址配置
* @param qrCodeImgCfg 二维码图配置
* @param conferee 与会人配置
* @param footer 页脚配置
* @param outputFile 图片输出位置
*/
public static void createCompositeImage(File backgroundImage, TextConfig title, TextConfig dateAddress,
QrCodeImageConfig qrCodeImgCfg, TextConfig conferee, TextConfig footer, File outputFile) {
try {
// 加载背景图片
BufferedImage bgImgBuffer = ImageIO.read(backgroundImage);
Graphics g = bgImgBuffer.getGraphics();
// 插入文字准备
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// 插入会议主题
y = title.getY();
drawString(title, g, g2d, bgImgBuffer.getWidth() - title.getX(), false);
// 插入日期、地点
y += 40;
drawString(dateAddress, g, g2d, bgImgBuffer.getWidth() - dateAddress.getX(), false);
// 插入二维码
y = Math.max(y + 30, 310);
g.drawImage(qrCodeImgCfg.getQrCodeBuffer(), (bgImgBuffer.getWidth() - qrCodeImgCfg.getWidth()) / 2, y, null);
// 插入与会人
y += 40 + qrCodeImgCfg.getImageHeight();
drawString(conferee, g, g2d, bgImgBuffer.getWidth(), true);
// 插入页脚
y = bgImgBuffer.getHeight() - 40;
drawString(footer, g, g2d, bgImgBuffer.getWidth(), true);
g.dispose();
// 将结果保存为新的图片
ImageIO.write(bgImgBuffer, "png", outputFile);
} catch (IOException e) {
log.error(e.getMessage());
} finally {
if (qrCodeImgCfg.getQrCodeImgFile().exists()) {
boolean delete = qrCodeImgCfg.getQrCodeImgFile().delete();
if (!delete) {
log.error("GenerateAttendanceImageUtil.createCompositeImage finally 二维码图片删除失败");
}
}
}
}
/**
* 绘制文字
*/
private static void drawString(TextConfig textConfig, Graphics g, Graphics2D g2d, Integer bgImgWidth, boolean horizontalCenter) {
g.setFont(new Font(textConfig.getTypeface(), textConfig.getBoldFont() ? Font.BOLD : Font.PLAIN, textConfig.getFontSize()));
g.setColor(textConfig.getFontColor());
FontMetrics fm = g2d.getFontMetrics();
int lineHeight = fm.getHeight();
int x = horizontalCenter ? (bgImgWidth - fm.stringWidth(textConfig.getText())) / 2 : textConfig.getX();
int lingNum = 1;
for (String text : convertToArray(textConfig.getText())) {
int textWidth = lingNum > 4 ? fm.stringWidth(text + "...") : fm.stringWidth(text);
boolean needTrim = false;
// 最多显示5行超出部分省略
if (x + textWidth > bgImgWidth) {
if (lingNum >= 5) {
g2d.drawString("...", x, y);
break;
}
// 符号截留在上一行,不做下一行的首字母
if (!Character.isLetterOrDigit(text.charAt(0)) && !Character.isWhitespace(text.charAt(0))) {
g2d.drawString(text, x, y);
continue;
}
x = textConfig.getX();
y += lineHeight + textConfig.getLineSpacing();
needTrim = true;
lingNum++;
}
// 取消换行后首字母的空格
if (needTrim && text.equals(" ")) {
continue;
}
g2d.drawString(text, x, y);
x += fm.stringWidth(text) + textConfig.getWordSpace();
}
}
/**
* 判断是否为英文或数字
*/
private static boolean isLetterOrNumber(char c) {
String character = String.valueOf(c);
return Pattern.matches("[a-zA-Z0-9]", character);
}
/**
* 将字符串拆分成最小单元
*/
public static String[] convertToArray(String input) {
List<String> resultList = new ArrayList<>();
for (int i = 0; i < input.length(); i++) {
char s = input.charAt(i);
if (isLetterOrNumber(s)) {
StringBuilder sb = new StringBuilder();
while (i < input.length() && isLetterOrNumber(input.charAt(i))) {
sb.append(input.charAt(i));
i++;
}
i--;
resultList.add(sb.toString());
} else {
resultList.add(String.valueOf(s));
}
}
return resultList.toArray(new String[0]);
}
}
注:
- 工具的核心方法有两个,一是
convertToArray()
函数,我们需要通过该方法将文本内容拆分成最小单元,每一个最小单元为数组中的一个元素。换行时需要通过判断元素和本行以生成内容的长度来判断是否进行换行。 - 二是
drawString()
函数,通过该函数实现了自动换行,符号截留,首字母空格消除等功能。 - 该工具类由于高度耦合需求所以没有抽取成一个大家都能公用的类库大家使用,需要使用的同学请根据自己的需求进行逻辑的调整。
测试类
import java.awt.*;
import java.io.File;
import java.util.UUID;
/**
* 测试 生成签到二维码图片
*/
public class Test {
public static void main(String[] args) {
File bgImg = new File("/Users/Desktop/background.png");
TextConfig title = new TextConfig("aaaaaaaaaaaaaaaaa aaaaaaaaaaaa aa aaaaaaaaaa, aaaaaaaaa, aaa aaaaaaaa aaaaaaaaaaaaaaaaa aaaaaaaaaaaa aa aaaaaaaaa aaaaaaaaaa aaaaaaaaaa",
"Arial", true, 24, Color.BLACK, 2, 1, 24, 144);
TextConfig dateAddress = new TextConfig("January 7, 2023-March 7, 2024 | Hong Kong, China", "Arial",
false, 16, Color.GRAY, 2, 0, 24);
QrCodeImageConfig qrCodeImgCfg = new QrCodeImageConfig("https://www.baidu.com", 200, 200,
Color.WHITE, new File("/Users/Desktop/" + UUID.randomUUID() + "qrcode.png"));
TextConfig conferee = new TextConfig("Hello World", "Arial", false, 30, Color.BLACK,
0, 0, 24);
TextConfig footer = new TextConfig("保存图片用于XXXX", "Arial", false, 14,
Color.GRAY, 0, 0, 24);
File outputFile = new File("/Users/Desktop/composite_image.png");
GenerateAttendanceImageUtil.createCompositeImage(bgImg, title, dateAddress, qrCodeImgCfg, conferee, footer, outputFile);
}
}
--------------------------------------------人生哪能多如意,万事只求半称心--------------------------------------------