Browse Source

cso报告适配

mamingxu 2 tuần trước cách đây
mục cha
commit
9b7f355892

+ 46 - 0
easier-report-biz/src/main/java/com/yaoyicloud/controller/CsoReportController.java

@@ -0,0 +1,46 @@
+package com.yaoyicloud.controller;
+
+import com.yaoyicloud.service.CsoReportService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.ByteArrayOutputStream;
+
+
+
+
+/**
+ * 报告控制器
+ *
+ * @author snows
+ * @date 2024/10/14
+ */
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+@RequestMapping("/cso")
+public class CsoReportController {
+    private final CsoReportService csoReportService;
+
+    @PostMapping(value = "/report/create-report", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+    public ResponseEntity<byte[]> handleReport(@RequestBody byte[] fileBytes) throws Exception {
+        // 1. 直接获取PDF字节流
+        ByteArrayOutputStream pdfStream = csoReportService.saveToTempFile(fileBytes);
+
+        // 2. 转换为字节数组
+        byte[] pdfBytes = pdfStream.toByteArray();
+
+        // 3. 直接返回PDF字节流
+        return ResponseEntity.ok()
+                .contentType(MediaType.APPLICATION_PDF)  // 关键:设置为PDF类型
+                .header("Content-Disposition", "attachment; filename=report.pdf")  // 设置下载文件名
+                .body(pdfBytes);
+    }
+}

+ 2 - 2
easier-report-biz/src/main/java/com/yaoyicloud/render/AntiBriberyRender.java

@@ -9,7 +9,7 @@ import com.deepoove.poi.config.Configure;
 import com.deepoove.poi.config.ConfigureBuilder;
 import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
 import com.google.protobuf.util.JsonFormat;
-import com.yaoyicloud.message.FxyProtos.AntiBribery;
+import com.yaoyicloud.message.FxyProtos;
 
 /**
  * AntiBribery渲染器
@@ -29,7 +29,7 @@ public final class AntiBriberyRender extends AbstractRender {
      * @return 本地文件目录
      * @throws IOException
      */
-    public String renderDocx(AntiBribery info, Map<String, Object> addtionalMap, byte[] templateFileContent) throws IOException {
+    public String renderDocx(FxyProtos.AntiBribery info, Map<String, Object> addtionalMap, byte[] templateFileContent) throws IOException {
         // 不需要定制展示逻辑的时候,使用protobuf的转json方法
         String jsonStr = JsonFormat.printer().print(info);
 

+ 14 - 0
easier-report-biz/src/main/java/com/yaoyicloud/service/CsoReportService.java

@@ -0,0 +1,14 @@
+package com.yaoyicloud.service;
+
+import java.io.ByteArrayOutputStream;
+
+/**
+ * 报告服务
+ *
+ * @author snows
+ * @date 2024/10/12
+ */
+public interface CsoReportService {
+
+    ByteArrayOutputStream saveToTempFile(byte[] fileBytes) throws Exception;
+}

+ 76 - 0
easier-report-biz/src/main/java/com/yaoyicloud/service/impl/CsoReportServiceImpl.java

@@ -0,0 +1,76 @@
+package com.yaoyicloud.service.impl;
+
+import com.yaoyicloud.config.FilerepoProperties;
+import com.yaoyicloud.service.CsoReportService;
+import com.yaoyicloud.tools.OfficeUtil1;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CsoReportServiceImpl implements CsoReportService {
+    private final FilerepoProperties filerepoProperties;
+
+    @Override
+    public ByteArrayOutputStream saveToTempFile(byte[] fileBytes) throws Exception {
+        // 1. 初始化路径和字体配置
+        String imagePath = filerepoProperties.getReportImagePath();
+        List<String> fontLists = Arrays.asList(filerepoProperties.getSourceHanSansCnFontMediumPath());
+        // 2. 创建临时Word文件(自动清理)
+        File reportTempWordFile = null;
+        try {
+            reportTempWordFile = File.createTempFile("temp_word_", ".docx");
+            Files.write(reportTempWordFile.toPath(), fileBytes);
+
+            String html = OfficeUtil1.convert(reportTempWordFile.getAbsolutePath(), imagePath);
+            try (BufferedWriter writer2 = new BufferedWriter(new FileWriter("1.html"))) {
+                writer2.write(html);
+            } catch (IOException e) {
+                System.err.println("写入 1.html 文件时发生错误: " + e.getMessage());
+                e.printStackTrace();
+            }
+            String html1 = OfficeUtil1.formatHtmlCso(html);
+            try (BufferedWriter writer2 = new BufferedWriter(new FileWriter("2.html"))) {
+                writer2.write(html1);
+            } catch (IOException e) {
+                System.err.println("写入 2.html 文件时发生错误: " + e.getMessage());
+                e.printStackTrace();
+            }
+            OfficeUtil1.convertHtmlToPdfCso(html1, fontLists, imagePath, true);
+            String html2 = OfficeUtil1.formatHtml(html);
+
+            try (BufferedWriter writer2 = new BufferedWriter(new FileWriter("3.html"))) {
+                writer2.write(html2);
+            } catch (IOException e) {
+                System.err.println("写入 3.html 文件时发生错误: " + e.getMessage());
+                e.printStackTrace();
+            }
+           return OfficeUtil1.convertHtmlToPdfCso(html2, fontLists, imagePath, false);
+        } catch (Exception e) {
+            log.error("文件转换失败", e);
+            throw e;
+        }
+    }
+
+
+    private void quietlyDelete(File file) {
+        if (file != null && file.exists()) {
+            try {
+                Files.delete(file.toPath());
+            } catch (IOException e) {
+                log.warn("无法删除临时文件 {}: {}", file.getAbsolutePath(), e.getMessage());
+            }
+        }
+    }
+}

+ 589 - 95
easier-report-biz/src/main/java/com/yaoyicloud/tools/OfficeUtil1.java

@@ -1,8 +1,14 @@
 package com.yaoyicloud.tools;
+
 import com.lowagie.text.Image;
 import com.lowagie.text.PageSize;
+
 import com.lowagie.text.pdf.BaseFont;
 import com.lowagie.text.pdf.PdfContentByte;
+import com.lowagie.text.pdf.PdfDictionary;
+
+import com.lowagie.text.pdf.PdfName;
+import com.lowagie.text.pdf.PdfObject;
 import com.lowagie.text.pdf.PdfReader;
 import com.lowagie.text.pdf.PdfStamper;
 import com.lowagie.text.pdf.parser.PdfTextExtractor;
@@ -23,7 +29,9 @@ import org.jsoup.nodes.Entities;
 import org.jsoup.select.Elements;
 import org.xhtmlrenderer.pdf.ITextFontResolver;
 import org.xhtmlrenderer.pdf.ITextRenderer;
+
 import java.awt.Color;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -43,8 +51,9 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 public class OfficeUtil1 {
-    private static final org.slf4j.Logger OFFICE_UTIL_LOGGER  = org.slf4j.LoggerFactory.getLogger(OfficeUtil1.class);
+    private static final org.slf4j.Logger OFFICE_UTIL_LOGGER = org.slf4j.LoggerFactory.getLogger(OfficeUtil1.class);
     private static Map<String, Integer> pageNumberMap = new LinkedHashMap<>();
+
     public static String convert(String docxPath, String imageDir) throws IOException {
         File imageDirFile = new File(imageDir);
         if (!imageDirFile.exists() && !imageDirFile.mkdirs()) {
@@ -148,7 +157,7 @@ public class OfficeUtil1 {
         }
 
         String baseCss =
-                 "@page {"
+                "@page {"
                         + "  size: A4;"
                         + "  @bottom-center {"
                         + "    content: none;"  // 只显示数字页码
@@ -163,7 +172,7 @@ public class OfficeUtil1 {
                         + "  }"
                         + "}"
                         + // 为最后一个div设置页码显示并重置计数器
-                         ".start-counting {"
+                        ".start-counting {"
                         + "  page: show-page-number;"
                         + "}"
                         + "td, th { "
@@ -318,14 +327,14 @@ public class OfficeUtil1 {
             } else {
                 secondarycurrentStyle += " margin-top: 13pt;";
             }
-            secondaryelement.attr("style",  secondarycurrentStyle + "line-height: 1.5; margin-bottom: 1pt; margin-left: 0.5em");
+            secondaryelement.attr("style", secondarycurrentStyle + "line-height: 1.5; margin-bottom: 1pt; margin-left: 0.5em");
         }
 
-       //三级标题样式
+        //三级标题样式
         Elements otherElements = doc.select("p.X1.X4");
         for (Element element : otherElements) {
             String style3 = element.attr("style");
-            element.attr("style",  style3 + "margin-top: 1pt !important; margin-bottom: 0pt  !important; margin-left: 0.5em");
+            element.attr("style", style3 + "margin-top: 1pt !important; margin-bottom: 0pt  !important; margin-left: 0.5em");
         }
         //六级标题样式
         Elements select1 = doc.select("p.X1.X6");
@@ -359,7 +368,167 @@ public class OfficeUtil1 {
         return doc.html();
     }
 
+    public static String formatHtmlCso(String html) {
+        Document doc = Jsoup.parse(html);
+        String baseCss =
+                "@page {"
+                        + "  size: A4;"
+                        + "  @bottom-center {"
+                        + "    content: none;"  // 只显示数字页码
+                        + "  }"
+                        + "  @top-left { content: element(pageHeader); }"
+                        + "}"
+                        + "@page :first {"
+                        + "  @top-left { content: none; }"  // 第一页不显示页眉
+                        + "}"
+                        + "@page show-page-number {"
+                        + "  @bottom-center {"
+                        + "    content: counter(page);"
+                        + "    font-family: 思源黑体 Medium;"
+                        + "    font-size: 9pt;"
+                        + "    color: #000000;"
+                        + "  }"
+                        + "}"
+                        + ".start-counting {"
+                        + "  page: show-page-number;"
+                        + "}"
+                        + "#pageHeader {"
+                        + "  position: running(pageHeader);"
+                        + "  transform-origin: left top;"  // 从左上角开始缩放
+                        + "}"
+                        + " "
+                        + ".watermark {"
+                        + "  position: fixed;"
+                        + "  top: 40%;"
+                        + "  left: 15%;"
+                        + "  opacity: 1;"
+                        + "  pointer-events: none;"
+                        + "  z-index: 1000;"
+                        + "}"
+                        + "td, th { "
+                        + "  page-break-inside: avoid; "
+                        + "  -fs-table-paginate: paginate; "
+                        + "  background-clip: padding-box; "
+                        + "  -webkit-print-color-adjust: exact; "
+                        + "}";
+
+        Elements table = doc.select("table");
+        String tbaleStyle = table.attr("style");
+        tbaleStyle += "width:100%; max-width: 100%;";
+        table.attr("style", tbaleStyle);
+
+        Elements trs = doc.select("tr");
+        for (Element tr : trs) {
+            String trStyle = tr.attr("style");
+            trStyle = (trStyle == null) ? "" : trStyle;
+            trStyle += " page-break-inside: avoid !important;"; // 强制不分页
+            tr.attr("style", trStyle);
+        }
+
+        doc.head().appendElement("style").text(baseCss);
+
+
+        Elements tds = doc.select("td");
+        for (Element td : tds) {
+            Elements ps = td.select("p");
+            for (Element p : ps) {
+                String originalStyle = p.attr("style");
+                String newStyle = "margin-left: 0.5em; margin-right: 0.5em; "
+                        + "line-height: 1.2; margin-top: 6px!important; margin-bottom: 6px!important; " + originalStyle;
+                p.attr("style", newStyle);
+            }
+            if (ps.size() > 1) {
+                for (int i = 1; i < ps.size(); i++) {
+                    ps.get(i).remove();
+                }
+                Element p = ps.first();
+                String pStyle = p.attr("style");
+                pStyle = removeWhiteSpacePreWrap(pStyle);
+                pStyle += " vertical-align: middle;";
+                p.attr("style", pStyle);
+            }
+
+            if (ps.size() > 0) {
+                Element p = ps.first();
+                String pStyle = p.attr("style");
+                pStyle = removeWhiteSpacePreWrap(pStyle);
+                p.attr("style", pStyle);
+
+                Elements spans = p.select("span");
+                if (!spans.isEmpty()) {
+                    for (Element span : spans) {
+                        String spanStyle = span.attr("style");
+                        spanStyle = removeWhiteSpacePreWrap(spanStyle);
+                        spanStyle = (spanStyle == null) ? "" : spanStyle;
+                        span.attr("style", spanStyle);
+                    }
+                } else {
+                    String oriPstyle = p.attr("style");
+                    oriPstyle = removeWhiteSpacePreWrap(oriPstyle);
+
+                    p.attr("style", oriPstyle);
+                }
+            }
+
+            String oristyle = td.attr("style");
+            oristyle = (oristyle == null) ? "" : oristyle;
+            oristyle += " border-collapse: collapse; border: 0.75pt solid #E3EDFB;";
+            oristyle += " background-clip: padding-box; break-inside: avoid !important; page-break-inside: avoid";
+            td.attr("style", oristyle);
+        }
+        Elements divs = doc.select("div");
+        divs.attr("style", "");
+        for (int i = divs.size() - 1; i >= 1; i--) {
+            Element div = divs.get(i);
+            div.attr("style", "page-break-before: always;");
+        }
+        divs.last().addClass("start-counting").attr("style", "-fs-page-sequence:start");
+        Elements images = doc.select("img");
+
+
+        Element firstImg = images.first();
+        // 4. 删除第一个img元素
+        firstImg.parent().remove();
+        // 将所有 white-space:pre-wrap 改为 normal去除转换时的奇怪空白
+        Elements allElements = doc.getAllElements();
+
+        for (Element element : allElements) {
+            String style = element.attr("style");
+            if (style.contains("white-space:pre-wrap")) {
+                style = style.replaceAll("white-space\\s*:\\s*[^;]+;", "");
+                element.attr("style", style);
+            }
+        }
+        Element firstDiv = doc.select("div").first();
+        if (firstDiv != null) {
+            firstDiv.addClass("first-page");
+        }
+
+        addTableOfContentsCso(doc);
+        doc.body().appendElement("div")
+                .attr("class", "watermark")
+                .appendElement("img")
+                .attr("src", "file:///C:/Users/yyy/Desktop/1.png")
+                .attr("width", "500");
+
+        doc.body().prependElement("div")
+                .attr("id", "pageHeader")
+                .appendElement("img")
+                .attr("src", "file:///C:/Users/yyy/Desktop/logo.png")
+                .attr("width", "47px");
+
+        Elements paragraphs = doc.select("p.X1.X2");
+        for (Element p : paragraphs) {
+            p.attr("style", p.attr("style") + "page-break-before:always; margin-top: 0pt !important;");
+
+        }
 
+        doc.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
+        doc.outputSettings().escapeMode(Entities.EscapeMode.xhtml);
+        doc.head().prepend("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">");
+
+        return doc.html();
+    }
 
     /**
      * 合并表格中相同内容的单元格
@@ -401,7 +570,7 @@ public class OfficeUtil1 {
     }
 
     /**
-     *     移除 white-space:pre-wrap 并替换为 normal
+     * 移除 white-space:pre-wrap 并替换为 normal
      */
     private static String removeWhiteSpacePreWrap(String style) {
         if (style == null) {
@@ -415,94 +584,96 @@ public class OfficeUtil1 {
         }
         return style.trim();
     }
+
     /**
      * 添加目录
+     *
      * @param doc
      */
     private static void addTableOfContents(Document doc) {
 
-            // 目录样式
-            String tocCss = ".toc-container { margin: 20px 0; font-family: 思源黑体 Medium; }"
-                    + ".toc-title { text-align: center; font-size: 12pt; margin-bottom: 15px; color: black; }"
-                    + ".toc-list { list-style-type: none; padding: 0; width: 100%; }"
-                    + ".toc-item { margin: 5px 0;     padding-top: 2px; padding-bottom: 2px;       line-height: 2;  }"
-                    + ".toc-level-1 { padding-left: 0; }"
-                    + ".toc-level-2 { padding-left: 2em; }"
-                    + ".toc-link { "
-                    + "  display: block; "
-                    + "  position: relative; "
-                    + "  color: black !important; "
-                    + "  text-decoration: none !important; "
-                    + "  line-height: 1.5; "  // 新增:控制整体行高
-                    + "}"
-                    + ".toc-line-container { "
-                    + "  display: table; "
-                    + "  width: 100%; "
-                    + "  vertical-align: middle; "  // 关键:控制容器内垂直对齐
-                    + "}"
-                    + ".toc-text { "
-                    + "  display: table-cell; "
-                    + "  font-size: 9pt; "
-                    + "  white-space: nowrap; "
-                    + "  padding-right: 5px; "
-                    + "  vertical-align: middle; "  // 改为middle对齐
-                    + "}"
-                    + ".toc-dots { "
-                    + "  display: table-cell; "
-                    + "  width: 100%; "
-                    + "  vertical-align: middle; "  // 关键:改为middle对齐
-                    + "  border-bottom: 1px dotted #000000; "
-                    + "  height: 1em; "  // 固定高度
-                    + "  margin-top: 2px; "  // 关键:正值下移,负值上移(按需调整)
-                    + "}"
-                    + "p.X1.X2 { -fs-pdf-bookmark: level 1; }"
-                    + "p.X1.X3 { -fs-pdf-bookmark: level 2; }"
-                    +  ".toc-page { "
-                    + "  display: table-cell; "
-                    + "  font-size: 9pt; "
-                    + "  white-space: nowrap; "
-                    + "  padding-left: 5px; "
-                    + "  vertical-align: middle; "  // 改为middle对齐
-                    + "}";
-            doc.head().appendElement("style").text(tocCss);
-
-            // 构建目录内容
-            Element tocList = new Element("ul").addClass("toc-list");
-            doc.select("p.X1.X2, p.X1.X3").forEach(el -> {
-                boolean isLevel1 = el.hasClass("X2");
-                String id = "sec_" + el.text().hashCode();
-                el.attr("id", id);
-                Integer pageNumber = pageNumberMap.getOrDefault(el.text(), 1);
-
-                Element li = tocList.appendElement("li")
-                        .addClass("toc-item " + (isLevel1 ? "toc-level-1" : "toc-level-2"));
-
-                Element link = li.appendElement("a")
-                        .attr("href", "#" + id)
-                        .addClass("toc-link");
-                Element lineContainer = link.appendElement("div").addClass("toc-line-container");
-                lineContainer.appendElement("span").addClass("toc-text").text(el.text());
-                lineContainer.appendElement("span").addClass("toc-dots");
-                lineContainer.appendElement("span").addClass("toc-page").text(String.valueOf(pageNumber));
-            });
-
-            // 插入目录
-            Element firstDiv = doc.select("div").first();
-            if (firstDiv != null) {
-                firstDiv.after(
-                        "<div class='toc-container' style='page-break-before: always;'>"
-                                + "<h1 class='toc-title'>目录</h1>"
-                                + tocList.outerHtml()
-                                + "</div>"
-                );
-            } else {
-                doc.body().prepend(
-                        "<div class='toc-container' style='page-break-before: always;'>"
-                                + "<h1 class='toc-title'>目录</h1>"
-                                + tocList.outerHtml()
-                                + "</div>"
-                );
-            }
+        // 目录样式
+        String tocCss = ".toc-container { margin: 20px 0; font-family: 思源黑体 Medium; }"
+                + ".toc-title { text-align: center; font-size: 12pt; margin-bottom: 15px; color: black; }"
+                + ".toc-list { list-style-type: none; padding: 0; width: 100%; }"
+                + ".toc-item { margin: 5px 0;     padding-top: 2px; padding-bottom: 2px;       line-height: 2;  }"
+                + ".toc-level-1 { padding-left: 0; }"
+                + ".toc-level-2 { padding-left: 2em; }"
+                + ".toc-link { "
+                + "  display: block; "
+                + "  position: relative; "
+                + "  color: black !important; "
+                + "  text-decoration: none !important; "
+                + "  line-height: 1.5; "  // 新增:控制整体行高
+                + "}"
+                + ".toc-line-container { "
+                + "  display: table; "
+                + "  width: 100%; "
+                + "  vertical-align: middle; "  // 关键:控制容器内垂直对齐
+                + "}"
+                + ".toc-text { "
+                + "  display: table-cell; "
+                + "  font-size: 9pt; "
+                + "  white-space: nowrap; "
+                + "  padding-right: 5px; "
+                + "  vertical-align: middle; "  // 改为middle对齐
+                + "}"
+                + ".toc-dots { "
+                + "  display: table-cell; "
+                + "  width: 100%; "
+                + "  vertical-align: middle; "  // 关键:改为middle对齐
+                + "  border-bottom: 1px dotted #000000; "
+                + "  height: 1em; "  // 固定高度
+                + "  margin-top: 2px; "  // 关键:正值下移,负值上移(按需调整)
+                + "}"
+                + "p.X1.X2 { -fs-pdf-bookmark: level 1; }"
+                + "p.X1.X3 { -fs-pdf-bookmark: level 2; }"
+                + ".toc-page { "
+                + "  display: table-cell; "
+                + "  font-size: 9pt; "
+                + "  white-space: nowrap; "
+                + "  padding-left: 5px; "
+                + "  vertical-align: middle; "  // 改为middle对齐
+                + "}";
+        doc.head().appendElement("style").text(tocCss);
+
+        // 构建目录内容
+        Element tocList = new Element("ul").addClass("toc-list");
+        doc.select("p.X1.X2, p.X1.X3").forEach(el -> {
+            boolean isLevel1 = el.hasClass("X2");
+            String id = "sec_" + el.text().hashCode();
+            el.attr("id", id);
+            Integer pageNumber = pageNumberMap.getOrDefault(el.text(), 1);
+
+            Element li = tocList.appendElement("li")
+                    .addClass("toc-item " + (isLevel1 ? "toc-level-1" : "toc-level-2"));
+
+            Element link = li.appendElement("a")
+                    .attr("href", "#" + id)
+                    .addClass("toc-link");
+            Element lineContainer = link.appendElement("div").addClass("toc-line-container");
+            lineContainer.appendElement("span").addClass("toc-text").text(el.text());
+            lineContainer.appendElement("span").addClass("toc-dots");
+            lineContainer.appendElement("span").addClass("toc-page").text(String.valueOf(pageNumber));
+        });
+
+        // 插入目录
+        Element firstDiv = doc.select("div").first();
+        if (firstDiv != null) {
+            firstDiv.after(
+                    "<div class='toc-container' style='page-break-before: always;'>"
+                            + "<h1 class='toc-title'>目录</h1>"
+                            + tocList.outerHtml()
+                            + "</div>"
+            );
+        } else {
+            doc.body().prepend(
+                    "<div class='toc-container' style='page-break-before: always;'>"
+                            + "<h1 class='toc-title'>目录</h1>"
+                            + tocList.outerHtml()
+                            + "</div>"
+            );
+        }
 
     }
 
@@ -561,10 +732,11 @@ public class OfficeUtil1 {
     }
 
     /**
-     *  操作已生成的pdf
-     * @param inputPdfPath   输入pdf
-     * @param outputPdfPath  输出pdf
-     * @param backgroundImagePath  图片文件夹位置
+     * 操作已生成的pdf
+     *
+     * @param inputPdfPath           输入pdf
+     * @param outputPdfPath          输出pdf
+     * @param backgroundImagePath    图片文件夹位置
      * @param onlyCollectPageNumbers 是否是遍历目录获取标题位置
      * @throws Exception
      */
@@ -576,7 +748,7 @@ public class OfficeUtil1 {
         PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(outputPdfPath));
         int startPage = 1;
         if (onlyCollectPageNumbers) {
-        pageNumberMap.clear();
+            pageNumberMap.clear();
             Pattern startPattern = Pattern.compile("^1\\.\\s+报告概述$");
 
             // 查找起始页
@@ -691,7 +863,7 @@ public class OfficeUtil1 {
         reader.close();
     }
 
-//    private static boolean isTableNearBottom(PdfWriter writer, PdfPTable table, float bottom) {
+    //    private static boolean isTableNearBottom(PdfWriter writer, PdfPTable table, float bottom) {
 //        try {
 //            // 获取当前页面的剩余高度
 //            float remainingHeight = writer.getVerticalPosition(true) - bottom;
@@ -707,5 +879,327 @@ public class OfficeUtil1 {
 //            return false;
 //        }
 //    }
+    public static ByteArrayOutputStream convertHtmlToPdfCso(String html, List<String> fontPaths, String imagePath, boolean flag) throws Exception {
+        ByteArrayOutputStream initialPdfStream = new ByteArrayOutputStream();
+        try {
+            ITextRenderer renderer = new ITextRenderer();
+            ITextFontResolver fontResolver = renderer.getFontResolver();
+
+            // 解析字体路径
+            String boldFont = null;
+            String regularFont = null;
+            String mediumFont = null;
+            for (String path : fontPaths) {
+                if (path.contains("bold")) {
+                    boldFont = path;
+                } else if (path.contains("medium")) {
+                    mediumFont = path;
+                } else if (path.contains("regular")) {
+                    regularFont = path;
+                }
+            }
+
+            // 添加字体
+            fontResolver.addFont(
+                    mediumFont,
+                    "思源黑体 Medium",
+                    BaseFont.IDENTITY_H,
+                    true,
+                    null
+            );
+            fontResolver.addFont(
+                    mediumFont,
+                    "Arial",
+                    BaseFont.IDENTITY_H,
+                    true,
+                    null
+            );
+
+            // 处理路径中的反斜杠
+            html = html.replace("C:\\", "file:///C:/")
+                    .replace("\\", "/");
+
+            // 设置 HTML 并渲染
+            renderer.setDocumentFromString(html, "file:///");
+            renderer.layout();
+            renderer.createPDF(initialPdfStream);
+        } catch (Exception e) {
+            initialPdfStream.close();
+            throw e;
+        }
+
+        // 第二步:处理初始 PDF 流并返回结果
+        ByteArrayOutputStream finalPdfStream = new ByteArrayOutputStream();
+        try {
+            ByteArrayInputStream initialPdfInputStream = new ByteArrayInputStream(initialPdfStream.toByteArray());
+            pdfReaderCso(initialPdfInputStream, finalPdfStream, imagePath + File.separator + "cosBackgroundImage.png", flag);
+            return finalPdfStream;
+        } catch (Exception e) {
+            finalPdfStream.close();
+            throw e;
+        } finally {
+            initialPdfStream.close();
+        }
+    }
+
+
+    /**
+     * 操作已生成的pdf
+     *
+     * @param backgroundImagePath    图片文件夹位置
+     * @param onlyCollectPageNumbers 是否是遍历目录获取标题位置
+     * @throws Exception
+     */
+    private static void pdfReaderCso(InputStream inputStream, OutputStream outputStream,
+                                     String backgroundImagePath, boolean onlyCollectPageNumbers)
+            throws Exception {
+        PdfReader reader = new PdfReader(inputStream);
+        PdfStamper stamper = new PdfStamper(reader, outputStream);
+        int startPage = 1;
+        if (onlyCollectPageNumbers) {
+            pageNumberMap.clear();
+            // 查找起始页
+            for (int pageNum = 1; pageNum <= reader.getNumberOfPages(); pageNum++) {
+                String pageText = new PdfTextExtractor(reader).getTextFromPage(pageNum);
+                String[] lines = pageText.split("\\r?\\n");
+                for (String line : lines) {
+                    if (line.equals("1.产品介绍")) {
+                        startPage = pageNum;
+                        pageNumberMap.put("startPage", startPage);
+                    }
+                }
+            }
+            // 收集标题和页码
+            Pattern titlePatterncso = Pattern.compile(
+                    "^((\\d+)\\.|(\\d+\\.\\d+))([\\u4e00-\\u9fa5a-zA-Z0-9].*)$",
+                    Pattern.MULTILINE
+            );
+
+            for (int pageNum = startPage; pageNum <= reader.getNumberOfPages(); pageNum++) {
+
+                String pageText = new PdfTextExtractor(reader).getTextFromPage(pageNum);
+                String[] lines = pageText.split("\\r?\\n");
+                for (int i = 0; i < lines.length; i++) {
+                    String line = lines[i].trim();
+                    if (line.isEmpty()) {
+                        continue;
+                    }
+                    Matcher matcher = titlePatterncso.matcher(line);
+                    if (matcher.matches()) {
+                        pageNumberMap.put(line, pageNum - startPage + 1);
+                    }
+                }
+            }
+        }
+        //删除第一页水印 避免边删边便利引入list
+        for (int pageNum = 1; pageNum <= reader.getNumberOfPages(); pageNum++) {
+            if (pageNum == 1) {
+                PdfDictionary pageDict = reader.getPageN(1);
+                PdfDictionary resources = pageDict.getAsDict(PdfName.RESOURCES);
+                PdfDictionary xobjects = resources.getAsDict(PdfName.XOBJECT);
+
+                if (xobjects != null) {
+                    // 用 List 存待删除的 PdfName
+                    List<PdfName> toRemove = new ArrayList<>();
+
+                    for (Object nameObj : xobjects.getKeys()) {
+                        PdfName name = (PdfName) nameObj;
+                        PdfObject obj = xobjects.get(name);
+
+                        if (obj.isIndirect()) {
+                            PdfDictionary xobj = (PdfDictionary) PdfReader.getPdfObject(obj);
+                            if (PdfName.IMAGE.equals(xobj.getAsName(PdfName.SUBTYPE))) {
+                                // 标记需要删除的名称,不直接删
+                                toRemove.add(name);
+                            }
+                        }
+                    }
+
+                    // 遍历结束后,统一删除标记的元素
+                    for (PdfName name : toRemove) {
+                        xobjects.remove(name);
+                    }
+                }
+            }
+            if (pageNum != 1) {
+                PdfContentByte underContent = stamper.getUnderContent(pageNum);
+
+                // 固定位置参数(可根据需要调整)
+                float pageWidth = reader.getPageSize(pageNum).getWidth();
+                float pageHeight = reader.getPageSize(pageNum).getHeight();
+                float xPos = 50; // 左侧边距
+                float yPos = pageHeight - 60; // 距离顶部50单位
+
+                underContent.saveState();
+                underContent.setColorStroke(new Color(49, 137, 186)); // 浅蓝色线条
+                underContent.setLineWidth(0.5f); // 线宽
+                underContent.moveTo(xPos - 10, yPos + 35);
+                underContent.lineTo(pageWidth - xPos + 10, yPos + 35);
+                underContent.stroke();
+                underContent.restoreState();
+            }
+        }
+        //一级标题图形背景
+        Pattern firstLevelTitlePattern = Pattern.compile("^(\\d+)\\.([\\u4e00-\\u9fa5a-zA-Z].*)$");
+        Set<Integer> styledPages = new HashSet<>();
+        startPage = pageNumberMap.get("startPage");
+        for (Map.Entry<String, Integer> stringIntegerEntry : pageNumberMap.entrySet()) {
+            String key = stringIntegerEntry.getKey();
+            int value = stringIntegerEntry.getValue();
+            if (firstLevelTitlePattern.matcher(key).find()) {
+                styledPages.add(value + startPage - 1);
+            }
+        }
+
+        // 在识别出的页面添加标题样式
+        for (Integer pageNum : styledPages) {
+            if (pageNum < 1 || pageNum > reader.getNumberOfPages()) {
+                continue;
+            }
+            PdfContentByte underContent = stamper.getUnderContent(pageNum);
+
+            // 固定位置参数(可根据需要调整)
+            float pageWidth = reader.getPageSize(pageNum).getWidth();
+            float pageHeight = reader.getPageSize(pageNum).getHeight();
+            float xPos = 50; // 左侧边距
+            float yPos = pageHeight - 60; // 距离顶部50单位
+
+            // 1. 绘制浅蓝色小方块(对应示例图里的小矩形)
+            underContent.saveState();
+            underContent.setColorFill(new Color(49, 137, 186)); // 更浅的蓝色,贴近示例图小方块色调
+            // 小方块尺寸,可微调
+            underContent.roundRectangle(
+                    xPos,
+                    yPos,
+                    10,
+                    10,
+                    2
+
+            );
+            underContent.fill();
+            underContent.restoreState();
+
+            // 2. 绘制深蓝色大方块(对应示例图里的主矩形)
+            underContent.saveState();
+            underContent.setColorFill(new Color(0, 82, 204)); // 深蓝色,贴近示例图主方块色调
+            // 大方块位置和尺寸,基于小方块对齐,可微调
+            underContent.roundRectangle(
+                    xPos - 10,   // 相对小方块偏移,让视觉更协调
+                    yPos - 10,
+                    16,
+                    16,
+                    3
+            );
+            underContent.fill();
+            underContent.restoreState();
 
+            // 2. 绘制横线
+            underContent.saveState();
+            underContent.setColorStroke(new Color(0x16, 0x77, 0xFF)); // 浅蓝色线条
+            underContent.setLineWidth(1.5f); // 线宽
+            underContent.moveTo(xPos - 10, yPos - 20);
+            underContent.lineTo(pageWidth - xPos + 10, yPos - 20);
+            underContent.stroke();
+            underContent.restoreState();
+
+        }
+
+        //封面背景
+        PdfContentByte background = stamper.getUnderContent(1);
+        Image image = Image.getInstance(backgroundImagePath);
+        image.scaleAbsolute(PageSize.A4.getWidth(), PageSize.A4.getHeight());
+        image.setAbsolutePosition(0, 0);
+        background.addImage(image);
+
+        stamper.close();
+        reader.close();
+    }
+
+    private static void addTableOfContentsCso(Document doc) {
+
+        // 目录样式
+        String tocCss = ".toc-container { margin: 20px 0; font-family: 思源黑体 Medium; }"
+                + ".toc-title { text-align: center; font-size: 12pt; margin-bottom: 15px; color: black; }"
+                + ".toc-list { list-style-type: none; padding: 0; width: 100%; }"
+                + ".toc-item { margin: 5px 0;     padding-top: 2px; padding-bottom: 2px;       line-height: 2;  }"
+                + ".toc-level-1 { padding-left: 0; }"
+                + ".toc-level-2 { padding-left: 2em; }"
+                + ".toc-link { "
+                + "  display: block; "
+                + "  position: relative; "
+                + "  color: black !important; "
+                + "  text-decoration: none !important; "
+                + "  line-height: 1.5; "  // 新增:控制整体行高
+                + "}"
+                + ".toc-line-container { "
+                + "  display: table; "
+                + "  width: 100%; "
+                + "  vertical-align: middle; "  // 关键:控制容器内垂直对齐
+                + "}"
+                + ".toc-text { "
+                + "  display: table-cell; "
+                + "  font-size: 9pt; "
+                + "  white-space: nowrap; "
+                + "  padding-right: 5px; "
+                + "  vertical-align: middle; "  // 改为middle对齐
+                + "}"
+                + ".toc-dots { "
+                + "  display: table-cell; "
+                + "  width: 100%; "
+                + "  vertical-align: middle; "  // 关键:改为middle对齐
+                + "  border-bottom: 1px dotted #000000; "
+                + "  height: 1em; "  // 固定高度
+                + "  margin-top: 2px; "  // 关键:正值下移,负值上移(按需调整)
+                + "}"
+                + "p.X1.X2 { -fs-pdf-bookmark: level 1; }"
+                + "p.X1.X3 { -fs-pdf-bookmark: level 2; }"
+                + ".toc-page { "
+                + "  display: table-cell; "
+                + "  font-size: 9pt; "
+                + "  white-space: nowrap; "
+                + "  padding-left: 5px; "
+                + "  vertical-align: middle; "  // 改为middle对齐
+                + "}";
+        doc.head().appendElement("style").text(tocCss);
+
+        // 构建目录内容
+        Element tocList = new Element("ul").addClass("toc-list");
+        doc.select("p.X1.X2, p.X1.X3").forEach(el -> {
+            boolean isLevel1 = el.hasClass("X2");
+            String id = "sec_" + el.text().hashCode();
+            el.attr("id", id);
+            Integer pageNumber = pageNumberMap.getOrDefault(el.text(), 1);
+
+            Element li = tocList.appendElement("li")
+                    .addClass("toc-item " + (isLevel1 ? "toc-level-1" : "toc-level-2"));
+
+            Element link = li.appendElement("a")
+                    .attr("href", "#" + id)
+                    .addClass("toc-link");
+            Element lineContainer = link.appendElement("div").addClass("toc-line-container");
+            lineContainer.appendElement("span").addClass("toc-text").text(el.text());
+            lineContainer.appendElement("span").addClass("toc-dots");
+            lineContainer.appendElement("span").addClass("toc-page").text(String.valueOf(pageNumber));
+        });
+
+        // 插入目录
+        Element secondDiv = doc.select("div").get(1);
+
+        if (secondDiv != null) {
+            secondDiv.after(
+                    "<div class='toc-container' style='page-break-before: always;'>"
+                            + "<h1 class='toc-title'>目录</h1>"
+                            + tocList.outerHtml()
+                            + "</div>"
+            );
+        } else {
+            doc.body().prepend(
+                    "<div class='toc-container' style='page-break-before: always;'>"
+                            + "<h1 class='toc-title'>目录</h1>"
+                            + tocList.outerHtml()
+                            + "</div>"
+            );
+        }
+
+    }
 }

+ 4 - 4
easier-report-biz/src/main/resources/application.yml

@@ -113,8 +113,8 @@ logging:
     com.yaoyicloud.easier.filerepo.mapper: debug
 
 # 租户表维护
-#easier:
-#  office:
-#    pdf:
-#      rootPath: C:/Users/yyy/dev/yyc3/easier-be
+easier:
+  office:
+    pdf:
+      rootPath: C:/Users/yyy/dev/yyc3/easier-be
 

+ 3 - 3
easier-report-biz/src/test/java/com/yaoyicloud/render/test/TestPdf.java

@@ -56,13 +56,13 @@ public class TestPdf {
         String imageDir = "D:\\li312\\Desktop\\image";
 
         String docxHtml = convert(
-                "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\temp\\1882350890076909569_df857a4625734caaafadb6afeb67b155.docx",
+                "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\temp\\1932256473198157826_1e0c248426e94a6da07285ce36b2eaff.docx",
                 "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\file\\image");
         // String docxHtml = convert("C:\\Users\\yyy\\dev\\yyc3\\easier-be\\temp\\1922178317200191489_d60ec021eb6b497eb6360ea8d78d8245.docx",
         // "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\file\\image");
         // String docxHtml = convert("C:\\Users\\yyy\\Documents\\WXWork\\1688856358659369\\Cache\\File\\2025-04\\cso.docx",
         // "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\file\\image");
-        try (BufferedWriter writer2 = new BufferedWriter(new FileWriter("2.html"))) {
+        try (BufferedWriter writer2 = new BufferedWriter(new FileWriter("3.html"))) {
             writer2.write(docxHtml);
         } catch (IOException e) {
             System.err.println("写入 2.html 文件时发生错误: " + e.getMessage());
@@ -70,7 +70,7 @@ public class TestPdf {
         }
 
         String docxHtml1 = OfficeUtil1.formatHtml(docxHtml);
-        try (BufferedWriter writer1 = new BufferedWriter(new FileWriter("1.html"))) {
+        try (BufferedWriter writer1 = new BufferedWriter(new FileWriter("4.html"))) {
             writer1.write(docxHtml1);
         } catch (IOException e) {
             System.err.println("写入 1.html 文件时发生错误: " + e.getMessage());