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; } } }