package com.dpkj.common.utils;
import com.alibaba.fastjson.JSONObject;
import com.dpkj.common.exception.RRException;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.StringTemplateResolver;
import org.w3c.dom.Document;
import org.xhtmlrenderer.layout.SharedContext;
import org.xhtmlrenderer.simple.Graphics2DRenderer;
import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* 模板服务类
*
* @author 石头人
* @version 1.0
* @since 2025-01-18 15:33:51
*/
@Service
@Slf4j
public class TemplateUtils {
/**
* 生成小票图片
*
* @param data json数据,用来填充模板
* @param template 模板(html字符串或者模板名称)
* @param width 图片宽度
* @param height 图片高度
* @param saveDir 图片的保存路径,如果为空,那么不进行图片的保存
* @return 图片字节数组
*/
public byte[] generateReceiptImage(JSONObject data, String template, int width, int height, StringBuilder saveDir) {
try {
// 获取模板上下文
Context context = this.getContext(data);
TemplateEngine templateEngine = new TemplateEngine();
if (checkIsHtml(template)) {
StringTemplateResolver resolver = new StringTemplateResolver();
resolver.setTemplateMode("HTML");
// 设置模板引擎使用这个自定义的模板解析器
templateEngine.setTemplateResolver(resolver);
}else {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCacheable(true);
resolver.setApplicationContext(new AnnotationConfigReactiveWebApplicationContext());
templateEngine.setTemplateResolver(resolver);
}
// 渲染模板
String html = templateEngine.process(template, context);
BufferedImage image = this.generate(html, width, height);
// 保存图片
if (saveDir != null && !"".equals(saveDir.toString())) {
String outputPath = saveDir.toString() + "\\genera_image_" + System.currentTimeMillis() + ".png";
ImageIO.write(image, "PNG", new File(outputPath));
saveDir.reverse();
saveDir.append(outputPath);
}
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", byteOutputStream);
return byteOutputStream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
throw new RRException("图片打印失败");
}
}
/**
* 生成图片
* @param html html内容
*/
private BufferedImage generate(String html, int width, int height){
try {
// 转换为xhtml
String xhtml = this.htmlToXhtml(html);
// 转换为document
Document document = this.xhtmlToDocument(xhtml);
// 生成图片
return createImageToDocument(document, width, height);
} catch (Exception e) {
e.printStackTrace();
throw new RRException("图片打印失败");
}
}
/**
* 通过JsonObject进行context内容填充
* @param data jsonObject
* @return context
*/
private Context getContext(JSONObject data) {
// 创建Thymeleaf上下文
Context context = new Context();
if ( data != null) {
Set keys = data.keySet();
for (String key : keys) {
// 判单是否有图片生成,统一后面采用的是_2base64Type
String[] split = key.split("_");
if (split.length > 1 && split[1].equals("2base64Type")) {
int width = split.length > 2 ? Integer.parseInt(split[2]) : 100;
int height = split.length > 3 ? Integer.parseInt(split[3]) : 100;
// 如果是图片类型,需要进行base64转换
String base64 = this.generateQRCode(String.valueOf(data.get(key)), width, height);
context.setVariable(split[0], "data:image/jpeg;base64," + base64);
} else {
// 普通字段直接设置
context.setVariable(key, data.get(key));
}
}
}
return context;
}
/**
* 根据内容生成二维码
* @param content 转换内容
*/
private String generateQRCode(String content, int width, int height) {
try {
Map hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 设置字符编码为 UTF-8
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); // 设置纠错级别
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, width, height, hints);
BufferedImage qrImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
qrImage.createGraphics();
Graphics2D graphics = (Graphics2D) qrImage.getGraphics();
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, width, height);
graphics.setColor(Color.BLACK);
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
if (bitMatrix.get(i, j)) {
graphics.fillRect(i, j, 1, 1);
}
}
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(qrImage, "png", baos);
return Base64.getEncoder().encodeToString(baos.toByteArray());
}catch (Exception e){
log.error("二维码生成失败");
throw new RRException("二维码生成失败");
}
}
/**
* xhtml 转换为 Document
* @param xhtml xhtml
* @return document
* @throws Exception e
*/
private Document xhtmlToDocument(String xhtml) throws Exception {
// 创建DocumentBuilder
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
DocumentBuilder builder = factory.newDocumentBuilder();
// 解析XHTML字符串为Document
return builder.parse(new ByteArrayInputStream(xhtml.getBytes(StandardCharsets.UTF_8)));
}
/**
* 转换,将html转换为xhtml
* @param html html内容
* @return xhtml
*/
private String htmlToXhtml(String html) {
org.jsoup.nodes.Document doc = Jsoup.parse(html);
doc.outputSettings()
.syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml)
.charset(StandardCharsets.UTF_8);
// 不使用外部DTD
return "\n" +
"\n" +
"\n" +
"\n" +
"\n" +
"\n" +
"\n" +
doc.body().html() +
"\n\n" +
"";
}
/**
* 通过document转换为图片
* @param document doc
* @param width 图片的宽度
* @param height 图片的高度
* @return bufferedImage
*/
private BufferedImage createImageToDocument(Document document, int width, int height) {
try {
// 创建Dimension对象
Dimension dimension = new Dimension(width, height);
// 创建图片
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = (Graphics2D) image.getGraphics();
// 设置白色背景
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, width, height);
// 设置渲染提示
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// 创建渲染器
Graphics2DRenderer renderer = new Graphics2DRenderer();
renderer.setDocument(document, null);
// 设置渲染参数
SharedContext sharedContext = renderer.getSharedContext();
sharedContext.setInteractive(false);
sharedContext.setDPI(256f);
// 使用系统默认字体,假设系统默认字体支持UTF - 8
Font font = new Font(Font.DIALOG, Font.PLAIN, 20);
graphics.setFont(font);
// 使用Dimension对象进行布局
renderer.layout(graphics, dimension);
renderer.render(graphics);
graphics.dispose();
return image;
} catch (Exception e) {
throw new RRException("渲染图片失败", e);
}
}
/**
* 校验这个模板内容是不是html字符串,而非模板名称
* @param template template
* @return 是否是html字符串
*/
private boolean checkIsHtml(String template){
try {
String pattern = "<(\"[^\"]*\"|'[^']*'|[^'\">])*>";
Pattern r = Pattern.compile(pattern);
return r.matcher(template).find();
}catch (Exception e) {
return false;
}
}
/**
* 将document对象转换为字符串
* @param doc document
* @return document转换为的字符串
*/
private String documentToString(Document doc) {
try {
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(doc), new StreamResult(writer));
return writer.getBuffer().toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}