QRCodeUtil.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. package com.ruoyi.common.utils.poi;
  2. import com.alibaba.fastjson.JSON;
  3. import com.google.zxing.BarcodeFormat;
  4. import com.google.zxing.EncodeHintType;
  5. import com.google.zxing.MultiFormatWriter;
  6. import com.google.zxing.WriterException;
  7. import com.google.zxing.common.BitMatrix;
  8. import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
  9. import com.ruoyi.common.utils.StringUtils;
  10. import com.ruoyi.framework.config.RuoYiConfig;
  11. import org.apache.commons.collections4.CollectionUtils;
  12. import org.apache.poi.util.IOUtils;
  13. import org.apache.poi.util.Units;
  14. import org.apache.poi.xwpf.usermodel.XWPFDocument;
  15. import org.apache.poi.xwpf.usermodel.XWPFParagraph;
  16. import org.slf4j.Logger;
  17. import org.slf4j.LoggerFactory;
  18. import javax.imageio.ImageIO;
  19. import javax.servlet.http.HttpServletResponse;
  20. import java.awt.*;
  21. import java.awt.image.BufferedImage;
  22. import java.io.*;
  23. import java.util.ArrayList;
  24. import java.util.HashMap;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.zip.ZipEntry;
  28. import java.util.zip.ZipOutputStream;
  29. public class QRCodeUtil<T> {
  30. private static final Logger log = LoggerFactory.getLogger(QRCodeUtil.class);
  31. /**
  32. * 导出数据列表
  33. */
  34. private List<T> list;
  35. private String title;
  36. private static XWPFDocument doc;
  37. //CODE_WIDTH:二维码宽度,单位像素(适当提高分辨率,让文字更清晰)
  38. private static final int CODE_WIDTH = 400;
  39. //CODE_HEIGHT:二维码高度,单位像素
  40. private static final int CODE_HEIGHT = 400;
  41. //FONT_WIDTH:字体大小(稍微放大一点)
  42. private static final int FONT_WIDTH = 18;
  43. //FRONT_COLOR:二维码前景色,0x000000 表示黑色
  44. private static final int FRONT_COLOR = 0x000000;
  45. //BACKGROUND_COLOR:二维码背景色,0xFFFFFF 表示白色
  46. //演示用 16 进制表示,和前端页面 CSS 的取色是一样的,注意前后景颜色应该对比明显,如常见的黑白
  47. private static final int BACKGROUND_COLOR = 0xFFFFFF;
  48. /**
  49. * 实体对象
  50. */
  51. public Class<T> clazz;
  52. public QRCodeUtil(Class<T> clazz) {
  53. this.clazz = clazz;
  54. }
  55. private void init(List<T> list, String title) {
  56. if (CollectionUtils.isEmpty(list)) {
  57. list = new ArrayList<>();
  58. }
  59. this.list = list;
  60. this.title = title;
  61. doc = new XWPFDocument();
  62. }
  63. public void exportQRCode(HttpServletResponse response, List<T> list) {
  64. exportQRCode(response, list, StringUtils.EMPTY);
  65. }
  66. public void exportQRCode(HttpServletResponse response, List<T> list, String title) {
  67. response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
  68. response.setCharacterEncoding("utf-8");
  69. this.init(list, title);
  70. exportQRCode(response);
  71. }
  72. public void exportQRCode(HttpServletResponse response) {
  73. try {
  74. writeDoc();
  75. doc.write(response.getOutputStream());
  76. } catch (Exception e) {
  77. log.error("导出Word异常{}", e.getMessage());
  78. } finally {
  79. IOUtils.closeQuietly(doc);
  80. }
  81. }
  82. public static void createCodeToFile(String content, File codeImgFileSaveDir, String fileName) {
  83. try {
  84. if (StringUtils.isBlank(content) || StringUtils.isBlank(fileName)) {
  85. return;
  86. }
  87. content = content.trim();
  88. if (codeImgFileSaveDir == null || codeImgFileSaveDir.isFile()) {
  89. //二维码图片存在目录为空,默认放在桌面...
  90. codeImgFileSaveDir = new File(RuoYiConfig.getQRCodePath());
  91. }
  92. if (!codeImgFileSaveDir.exists()) {
  93. //二维码图片存在目录不存在,开始创建...
  94. codeImgFileSaveDir.mkdirs();
  95. }
  96. //核心代码-生成二维码
  97. BufferedImage bufferedImage = getBufferedImage(content);
  98. File codeImgFile = new File(codeImgFileSaveDir, fileName);
  99. ImageIO.write(bufferedImage, "png", codeImgFile);
  100. log.info("二维码图片生成成功:" + codeImgFile.getPath());
  101. } catch (Exception e) {
  102. e.printStackTrace();
  103. }
  104. }
  105. public static void createCodeToStream(String content, ByteArrayOutputStream baos, String topText, String bottomText) {
  106. try {
  107. if (StringUtils.isBlank(content)) {
  108. return;
  109. }
  110. content = content.trim();
  111. //核心代码-生成二维码
  112. BufferedImage qrCodeImage = getBufferedImage(content);
  113. // 计算总高度:顶部文字区域 + 二维码 + 底部文字区域
  114. int topMargin = StringUtils.isNotEmpty(topText) ? 40 : 0; // 顶部文字区域高度
  115. int bottomMargin = 80; // 底部文字区域高度,适当加大以容纳多行文字
  116. int totalHeight = CODE_HEIGHT + topMargin + bottomMargin;
  117. // 创建包含顶部和底部文字的组合图片
  118. BufferedImage combinedImage = new BufferedImage(CODE_WIDTH, totalHeight, BufferedImage.TYPE_INT_RGB);
  119. Graphics2D g = combinedImage.createGraphics();
  120. // 设置抗锯齿(图像和文字)
  121. g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  122. g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
  123. // 设置背景颜色为白色
  124. g.setColor(Color.WHITE);
  125. g.fillRect(0, 0, CODE_WIDTH, totalHeight);
  126. // 绘制顶部文字
  127. if (StringUtils.isNotEmpty(topText)) {
  128. g.setColor(Color.BLACK);
  129. g.setFont(new Font("微软雅黑", Font.PLAIN, FONT_WIDTH));
  130. FontMetrics fm = g.getFontMetrics();
  131. int textWidth = fm.stringWidth(topText);
  132. int textX = (CODE_WIDTH - textWidth) / 2;
  133. int textY = topMargin - 10; // 文字在顶部区域垂直居中
  134. g.drawString(topText, textX, textY);
  135. }
  136. // 绘制二维码(向下偏移顶部文字区域的高度)
  137. g.drawImage(qrCodeImage, 0, topMargin, null);
  138. // 绘制底部文字(支持自动换行)
  139. if (StringUtils.isNotEmpty(bottomText)) {
  140. g.setColor(Color.BLACK);
  141. g.setFont(new Font("微软雅黑", Font.PLAIN, FONT_WIDTH));
  142. FontMetrics fm = g.getFontMetrics();
  143. int maxWidth = CODE_WIDTH - 20; // 留左右 10 像素边距
  144. List<String> lines = new ArrayList<>();
  145. StringBuilder currentLine = new StringBuilder();
  146. for (int i = 0; i < bottomText.length(); i++) {
  147. char c = bottomText.charAt(i);
  148. currentLine.append(c);
  149. if (fm.stringWidth(currentLine.toString()) > maxWidth) {
  150. // 超出宽度,回退一个字符作为下一行开始
  151. currentLine.deleteCharAt(currentLine.length() - 1);
  152. lines.add(currentLine.toString());
  153. currentLine = new StringBuilder().append(c);
  154. }
  155. }
  156. if (currentLine.length() > 0) {
  157. lines.add(currentLine.toString());
  158. }
  159. int lineHeight = fm.getHeight();
  160. int startY = topMargin + CODE_HEIGHT + (bottomMargin - lineHeight * lines.size()) / 2 + fm.getAscent();
  161. for (int i = 0; i < lines.size(); i++) {
  162. String line = lines.get(i);
  163. int textWidth = fm.stringWidth(line);
  164. int textX = (CODE_WIDTH - textWidth) / 2;
  165. int textY = startY + i * lineHeight;
  166. g.drawString(line, textX, textY);
  167. }
  168. }
  169. g.dispose();
  170. ImageIO.write(combinedImage, "png", baos);
  171. log.info("二维码图片生成到输出流成功...");
  172. } catch (Exception e) {
  173. e.printStackTrace();
  174. }
  175. }
  176. private static Font loadFont(String fontFilePath, int fontSize) throws IOException, FontFormatException {
  177. return Font.createFont(Font.TRUETYPE_FONT, new File(fontFilePath)).deriveFont(Font.PLAIN, fontSize);
  178. }
  179. //核心代码-生成二维码
  180. private static BufferedImage getBufferedImage(String content) throws WriterException {
  181. //com.google.zxing.EncodeHintType:编码提示类型,枚举类型
  182. Map<EncodeHintType, Object> hints = new HashMap<>();
  183. //EncodeHintType.CHARACTER_SET:设置字符编码类型
  184. hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
  185. //EncodeHintType.ERROR_CORRECTION:设置误差校正
  186. //ErrorCorrectionLevel:误差校正等级,L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
  187. //不设置时,默认为 L 等级,等级不一样,生成的图案不同,但扫描的结果是一样的
  188. hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
  189. //EncodeHintType.MARGIN:设置二维码边距,单位像素,值越小,二维码距离四周越近
  190. hints.put(EncodeHintType.MARGIN, 1);
  191. MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
  192. BitMatrix bitMatrix = multiFormatWriter.encode(content, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints);
  193. BufferedImage bufferedImage = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_BGR);
  194. for (int x = 0; x < CODE_WIDTH; x++) {
  195. for (int y = 0; y < CODE_HEIGHT; y++) {
  196. bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? FRONT_COLOR : BACKGROUND_COLOR);
  197. }
  198. }
  199. return bufferedImage;
  200. }
  201. private void writeDoc() {
  202. XWPFParagraph paragraph = doc.createParagraph();
  203. list.forEach(item -> {
  204. try {
  205. log.info("==================================================" + JSON.toJSONString(item));
  206. //务必重写类的toString方法
  207. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  208. createCodeToStream(JSON.toJSONString(item), baos, title, item.toString());
  209. InputStream is = new ByteArrayInputStream(baos.toByteArray());
  210. paragraph.createRun().addPicture(is, XWPFDocument.PICTURE_TYPE_JPEG, "image.jpg", Units.toEMU(200), Units.toEMU(200));
  211. } catch (Exception e) {
  212. e.printStackTrace();
  213. }
  214. });
  215. }
  216. /**
  217. * 生成二维码图片并打包成ZIP文件下载
  218. *
  219. * @param response HttpServletResponse
  220. * @param list 数据列表
  221. * @param title 标题(可选)
  222. */
  223. public void exportQRCodeAsZip(HttpServletResponse response, List<T> list, String title) {
  224. response.setContentType("application/zip");
  225. response.setCharacterEncoding("utf-8");
  226. response.setHeader("Content-Disposition", "attachment;");
  227. this.init(list, title);
  228. try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
  229. // 设置压缩级别
  230. zos.setLevel(ZipOutputStream.STORED);
  231. int index = 1;
  232. for (T item : list) {
  233. try {
  234. // 生成文件名
  235. String fileName = generateFileName(item, index) + ".png";
  236. index++;
  237. // 创建ZIP条目
  238. ZipEntry zipEntry = new ZipEntry(fileName);
  239. zos.putNextEntry(zipEntry);
  240. // 生成二维码图片并写入ZIP
  241. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  242. String content = JSON.toJSONString(item);
  243. String bottomText = item.toString();
  244. createCodeToStream(content, baos, title, bottomText);
  245. // 将字节数组写入ZIP
  246. zos.write(baos.toByteArray());
  247. zos.closeEntry();
  248. baos.close();
  249. log.info("成功生成二维码图片: {}", fileName);
  250. } catch (Exception e) {
  251. log.error("生成二维码图片失败: {}", e.getMessage());
  252. }
  253. }
  254. zos.finish();
  255. log.info("ZIP文件生成成功,包含 {} 个二维码图片", list.size());
  256. } catch (Exception e) {
  257. log.error("生成ZIP文件异常: {}", e.getMessage());
  258. try {
  259. response.reset();
  260. response.setContentType("application/json");
  261. response.setCharacterEncoding("utf-8");
  262. response.getWriter().println("生成ZIP文件失败: " + e.getMessage());
  263. } catch (IOException ex) {
  264. log.error("发送错误信息失败: {}", ex.getMessage());
  265. }
  266. }
  267. }
  268. /**
  269. * 生成二维码图片并打包成ZIP文件下载(简化版)
  270. *
  271. * @param response HttpServletResponse
  272. * @param list 数据列表
  273. */
  274. public void exportQRCodeAsZip(HttpServletResponse response, List<T> list) {
  275. exportQRCodeAsZip(response, list, StringUtils.EMPTY);
  276. }
  277. /**
  278. * 生成文件名
  279. *
  280. * @param item 数据项
  281. * @param index 索引
  282. * @return 文件名
  283. */
  284. private String generateFileName(T item, int index) {
  285. // 如果对象有特定的标识字段,可以重写此方法
  286. // 例如:如果对象有name字段,可以返回 item.getName()
  287. // 默认使用索引+对象字符串表示
  288. String baseName = StringUtils.substring(item.toString(), 0, 20); // 限制长度
  289. baseName = baseName.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5]", "_"); // 替换特殊字符
  290. return String.format("%03d_%s", index, baseName);
  291. }
  292. }