Browse Source

build poi5+flyingsaucer 生成报告

mamingxu 1 tháng trước cách đây
mục cha
commit
e86b54b9da

+ 10 - 0
easier-report-biz/pom.xml

@@ -58,6 +58,16 @@
             <groupId>com.alibaba.cloud</groupId>
             <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.lowagie</groupId>
+            <artifactId>itext</artifactId>
+            <version>2.1.7</version>
+        </dependency>
+        <dependency>
+            <groupId>org.xhtmlrenderer</groupId>
+            <artifactId>flying-saucer-pdf</artifactId>
+            <version>9.12.0</version>
+        </dependency>
         <!-- iText 7 核心 -->
         <!--        <dependency>-->
         <!--            <groupId>com.itextpdf</groupId>-->

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

@@ -7,8 +7,7 @@ import java.util.UUID;
 import com.deepoove.poi.config.Configure;
 import com.deepoove.poi.config.ConfigureBuilder;
 import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
+
 import com.google.protobuf.util.JsonFormat;
 import com.yaoyicloud.message.FxyProtos.BasicInfo;
 

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

@@ -0,0 +1,566 @@
+package com.yaoyicloud.render.test;
+
+import com.lowagie.text.pdf.BaseFont;
+import fr.opensagres.poi.xwpf.converter.core.FileImageExtractor;
+import fr.opensagres.poi.xwpf.converter.core.FileURIResolver;
+import fr.opensagres.poi.xwpf.converter.core.ImageManager;
+import fr.opensagres.poi.xwpf.converter.xhtml.XHTMLConverter;
+import fr.opensagres.poi.xwpf.converter.xhtml.XHTMLOptions;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Entities;
+import org.jsoup.select.Elements;
+import org.xhtmlrenderer.pdf.ITextFontResolver;
+import org.xhtmlrenderer.pdf.ITextRenderer;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class TestPdf {
+
+    /**
+     * 使用poi+itextpdf进行word转pdf 先将word转成html,再将html转成pdf
+     */
+    @SuppressWarnings("checkstyle:UncommentedMain")
+    public static void main(String[] args) throws Exception {
+
+        String basePath = "C:\\Users\\yyy\\Desktop";
+        String docxPath = basePath + "\\ccc.docx";
+        String pdfPath = basePath + "ccc.pdf";
+        String imageDir = "D:\\li312\\Desktop\\image";
+
+        String docxHtml = convert(
+                "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\temp\\1917398743161577473_e512b527cc4e4604bdf359131cfb323a.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"))) {
+            writer2.write(docxHtml);
+        } catch (IOException e) {
+            System.err.println("写入 2.html 文件时发生错误: " + e.getMessage());
+            e.printStackTrace();
+        }
+        String docxHtml1 = formatHtml(docxHtml);
+        try (BufferedWriter writer1 = new BufferedWriter(new FileWriter("1.html"))) {
+            writer1.write(docxHtml1);
+        } catch (IOException e) {
+            System.err.println("写入 1.html 文件时发生错误: " + e.getMessage());
+            e.printStackTrace();
+        }
+
+
+        convertHtmlToPdf(docxHtml1, "./output.pdf");
+
+    }
+
+
+    /**
+     * 将docx转为html
+     *
+     * @param docxPath 输入docx文件路径
+     * @return 生成的HTML内容
+     */
+    // 日志记录器
+    private static final org.slf4j.Logger OFFICE_UTIL_LOGGER  = org.slf4j.LoggerFactory.getLogger(TestPdf.class);
+
+
+    public static String convert(String docxPath, String imageDir) throws IOException {
+        File imageDirFile = new File(imageDir);
+        if (!imageDirFile.exists() && !imageDirFile.mkdirs()) {
+            throw new IOException("无法创建图片目录: " + imageDir);
+        }
+
+        try (InputStream docxIn = new FileInputStream(docxPath);
+             XWPFDocument document = new XWPFDocument(docxIn);
+             ByteArrayOutputStream htmlOut = new ByteArrayOutputStream()) {
+
+            // 执行转换
+            XHTMLOptions options = createHtmlOptions(imageDirFile);
+            XHTMLConverter.getInstance().convert(document, htmlOut, options);
+
+            return htmlOut.toString("UTF-8");
+        } catch (Exception e) {
+            OFFICE_UTIL_LOGGER.error("转换失败: {}", e.getMessage(), e);
+            throw new IOException("DOCX转换失败", e);
+        }
+    }
+
+
+    /**
+     * 创建HTML转换选项
+     */
+    private static XHTMLOptions createHtmlOptions(File imageDirFile) {
+        @SuppressWarnings("deprecation")
+        XHTMLOptions options = XHTMLOptions.create()
+                .setImageManager(new ImageManager(imageDirFile, "") {
+                    @Override
+                    public String resolve(String uri) {
+                        return new File(imageDirFile, uri).getAbsolutePath().replace("/", "\\");
+                    }
+                })
+                .URIResolver(new FileURIResolver(imageDirFile) {
+                    @Override
+                    public String resolve(String uri) {
+                        String filename = uri.replace("word/media/", "");
+                        return new File(imageDirFile, filename).getAbsolutePath().replace("/", "\\");
+                    }
+                });
+
+        options.setIgnoreStylesIfUnused(false);
+        options.setExtractor(new FileImageExtractor(imageDirFile));
+        return options;
+    }
+
+    /**
+     * 使用jsoup规范化html
+     *
+     * @param html html内容
+     * @return 规范化后的html
+     */
+
+    public static String formatHtml(String html) {
+        Document doc = Jsoup.parse(html);
+
+        removeEmptyParagraphs(doc);
+        String baseCss = "@page {"
+                + "  size: A4;"
+                + "  @bottom-center {"
+                + "    content: \"第 \" counter(page) \" 页,共 \" counter(pages) \" 页\";"
+                + "    font-family: 思源黑体;"
+                + "    font-size: 10pt;"
+                + "    color: #000000;"
+                + "  }"
+                + "}"
+                + "body { font-family: 思源黑体; }"
+                + "table {"
+                + "  width: 100%;"
+                + "  border-collapse: collapse;"
+                + "  page-break-inside: auto;"
+                + "  -fs-table-paginate: paginate;"
+                + "}"
+                + "thead {"
+                + "  display: table-header-group;"
+                + "}"
+                + "td, th {"
+                + "  -fs-table-paginate: paginate;"
+                + "  background-clip: padding-box;"
+                + "  -webkit-print-color-adjust: exact;"
+                + "}"
+                + ".avoid-break {"
+                + "  break-inside: avoid;"
+                + "  page-break-inside: avoid;"
+                + "}"
+                + "p.X1.X2 {"
+                + "  -fs-pdf-bookmark-level: 1;"
+                + "  -fs-pdf-bookmark-open: true;"
+                + "}"
+                + "p.X1.X3 {"
+                + "  -fs-pdf-bookmark-level: 2;"
+                + "}";
+
+        doc.head().appendElement("style").text(baseCss);
+        processFirstImageAsBackground(doc);
+
+
+        processTableCells(doc);
+        Elements divs = doc.select("div");
+        divs.attr("style", "");
+
+        addTableOfContents(doc);
+        // 7. 处理特殊span元素
+        Elements spans = doc.select("span.X1.X2");
+        for (Element span : spans) {
+            String style = span.attr("style");
+            style = style.replaceAll("margin-left:\\s*[^;]+;?", "");
+            if (!span.text().contains("重要声明")) {
+                style += "color:#1677ff; ";
+            }
+            span.attr("style", style);
+        }
+        // 8. 一级标题前分页样式
+        Elements paragraphs = doc.select("p.X1.X2");
+        for (Element p : paragraphs) {
+            p.attr("style", p.attr("style") + "page-break-before:always;");
+        }
+
+        processTables(doc);
+        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();
+    }
+
+    /**
+     * 删除空白段落 清除因转换出的空白段落造成的空白页
+     * @param doc
+     */
+    private static void removeEmptyParagraphs(Document doc) {
+        Elements pTags = doc.select("p");
+        for (Element p : pTags) {
+            boolean isValidEmpty = true;
+
+            for (org.jsoup.nodes.Node child : p.childNodes()) {
+                if (child instanceof Element) {
+                    if (!((Element) child).tagName().equalsIgnoreCase("br")) {
+                        isValidEmpty = false;
+                        break;
+                    }
+                } else {
+                    if (!child.outerHtml().trim().isEmpty()) {
+                        isValidEmpty = false;
+                        break;
+                    }
+                }
+            }
+
+            if (isValidEmpty) {
+                p.remove();
+            }
+        }
+    }
+    /**
+     * 处理第一张图片作为背景
+     * @param doc
+     */
+    private static void processFirstImageAsBackground(Document doc) {
+        Element firstDiv = doc.select("div").first();
+        if (firstDiv != null) {
+            Element firstImg = firstDiv.select("img").first();
+            if (firstImg != null) {
+                String imgSrc = firstImg.absUrl("src");
+
+                Element pageContainer = new Element("div")
+                        .attr("style",
+                                "position: relative;"
+                                        + "width: 100%;"
+                                        + "height: 100vh;"
+                                        + "page-break-after: always;"
+                                        + "overflow: hidden;");
+
+                Element backgroundLayer = new Element("div")
+                        .attr("style",
+                                "position: absolute;"
+                                        + "top: 0;"
+                                        + "left: 0;"
+                                        + "width: 100%;"
+                                        + "height: 100%;"
+                                        + "background-image: url('" + imgSrc + "');"
+                                        + "background-size: cover;"
+                                        + "background-position: center;"
+                                        + "background-repeat: no-repeat;"
+                                        + "z-index: 0;");
+
+                Element contentContainer = new Element("div")
+                        .attr("style",
+                                "position: relative;"
+                                        + "z-index: 1;"
+                                        + "height: 100%;"
+                                        + "width: 100%;")
+                        .html(firstDiv.html());
+
+                firstImg.remove();
+                pageContainer.appendChild(backgroundLayer);
+                pageContainer.appendChild(contentContainer);
+                firstDiv.replaceWith(pageContainer);
+            }
+        }
+    }
+    /**
+     * 添加目录
+     * @param doc
+     */
+    private static void addTableOfContents(Document doc) {
+        // 目录样式
+        String tocCss = ".toc-container { margin: 20px 0; }"
+                + ".toc-title { "
+                + "  text-align: center; "
+                + "  font-size: 18pt; "
+                + "  margin-bottom: 15px; "
+                + "  color: black;"
+                + "}"
+                + ".toc-list { "
+                + "  list-style-type: none; "
+                + "  padding: 0; "
+                + "  width: 100%; "
+                + "}"
+                + ".toc-item { "
+                + "  margin: 5px 0; "
+                + "}"
+                + ".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; "
+                + "}"
+                + ".toc-content { "
+                + "  display: flex; "
+                + "}"
+                + ".toc-text { "
+                + "  white-space: normal; "
+                + "}"
+                + ".toc-dots { "
+                + "  vertical-align: bottom; "
+                + "  min-width: 20px; "
+                + "  border-bottom: 1px dotted #000000; "
+                + "  margin: 0 5px; "
+                + "  height: 1em; "
+                + "  flex-grow: 1; "
+                + "}"
+                + ".toc-page { "
+                + "  position: absolute; "
+                + "  right: 0; "
+                + "  bottom: 0; "
+                + "}";
+        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);
+
+            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 content = link.appendElement("span").addClass("toc-content");
+            content.appendElement("span").addClass("toc-text").text(el.text());
+            content.appendElement("span").addClass("toc-dots");
+            content.appendElement("span").addClass("toc-page").text("1");
+        });
+
+        // 插入目录
+        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>"
+            );
+        }
+    }
+
+    /**
+     * 处理表格单元格样式
+     * @param doc
+     */
+    private static void processTableCells(Document doc) {
+        Elements tds = doc.select("td");
+        for (Element td : tds) {
+            Elements ps = td.select("p");
+            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 += " display: table-cell; 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;
+                        spanStyle += " margin-left: 0.5em;";
+                        span.attr("style", spanStyle);
+                    }
+                } else {
+                    String oriPstyle = p.attr("style");
+                    oriPstyle = removeWhiteSpacePreWrap(oriPstyle);
+                    p.attr("style", oriPstyle + " margin-left: 0.5em;");
+                }
+            }
+
+            String oristyle = td.attr("style");
+            oristyle = (oristyle == null) ? "" : oristyle;
+            oristyle += " border-collapse: collapse; border: 0.75pt solid #E3EDFB;";
+            oristyle += " background-clip: padding-box;";
+            td.attr("style", oristyle);
+        }
+    }
+    /**
+     * 处理表格头
+     * @param doc
+     */
+    @SuppressWarnings("checkstyle:NestedIfDepth")
+    private static void processTables(Document doc) {
+        Elements tables = doc.select("table");
+        tables.attr("style", tables.attr("style") + "width:100%;");
+
+        for (Element table : tables) {
+            Elements tbody = table.select("tbody");
+            if (!tbody.isEmpty()) {
+                Elements rows = tbody.select("tr");
+                if (rows.size() >= 2) {
+                    Element firstRow = rows.get(0);
+                    Element secondRow = rows.get(1);
+
+                    Element firstCell = firstRow.selectFirst("td, th");
+                    String firstCellText = "";
+
+                    if (firstCell != null) {
+                        firstCellText = firstCell.text().trim();
+                        Element span = firstCell.selectFirst("span");
+                        if (span != null && "科目".equals(span.text().trim())) {
+                            boolean secondRowIsHeader = isHeaderRow(secondRow);
+                            if (secondRowIsHeader) {
+                                Element thead = table.prependElement("thead").addClass("table-header");
+                                thead.appendChild(firstRow.clone());
+                                thead.appendChild(secondRow.clone());
+                                firstRow.remove();
+                                secondRow.remove();
+                                continue;
+                            }
+                        }
+                    }
+
+                    if (isHeaderRow(firstRow)) {
+                        Element thead = table.prependElement("thead").addClass("table-header");
+                        thead.appendChild(firstRow.clone());
+                        firstRow.remove();
+                    }
+                }
+            }
+
+            for (Element td : table.select("td")) {
+                td.addClass("avoid-break");
+            }
+        }
+    }
+
+    /**
+     *     移除 white-space:pre-wrap 并替换为 normal
+     */
+    private static String removeWhiteSpacePreWrap(String style) {
+        if (style == null) {
+            return "";
+        }
+        // 替换 pre-wrap 为 normal,并去除多余的分号
+        style = style.replaceAll("white-space\\s*:\\s*pre-wrap\\s*;?", "");
+        style = style.replaceAll(";\\s*;", ";"); // 清理多余分号
+        if (!style.contains("white-space")) {
+            style += " white-space: normal;";
+        }
+        return style.trim();
+    }
+
+    /**
+     * 检查行是否为表头(所有单元格使用"思源黑体")
+     */
+    private static boolean isHeaderRow(Element row) {
+        Elements tds = row.select("td");
+        for (Element td : tds) {
+            // 跳过空单元格(可能是合并单元格的占位)
+            if (td.text().trim().isEmpty()) {
+                continue;
+            }
+
+            Element span = td.select("p > span").first();
+            if (span == null || !hasFontFamily(span, "思源黑体", false)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 检查元素的 style 属性中是否包含指定的 font-family
+     */
+    private static final Pattern FONT_FAMILY_PATTERN = Pattern.compile(
+            "font-family\\s*:\\s*'?(思源黑体)([^'\";]*)'?");
+
+    private static boolean hasFontFamily(Element element, String fontFamily, boolean allowWeight) {
+        if (element == null) {
+            return false;
+        }
+
+        String style = element.attr("style");
+        Matcher matcher = FONT_FAMILY_PATTERN.matcher(style);
+
+        if (matcher.find()) {
+            String baseFont = matcher.group(1);  // 捕获的基础字体名(如"思源黑体")
+            String weightSuffix = matcher.group(2);  // 捕获的字重后缀(可能为空)
+
+            // 如果不允许字重后缀,则必须完全匹配
+            return baseFont.equals(fontFamily) && (!allowWeight ? weightSuffix.isEmpty() : true);
+        }
+        return false;
+    }
+
+
+    public static void convertHtmlToPdf(String html, String outputPdfPath) throws Exception {
+        try (OutputStream os = new FileOutputStream(outputPdfPath)) {
+            ITextRenderer renderer = new ITextRenderer();
+            ITextFontResolver fontResolver = renderer.getFontResolver();
+
+            // 字体路径
+            String mediumFont = "C:/Users/yyy/AppData/Local/Microsoft/Windows/Fonts/SourceHanSansSC-Medium-2.otf";
+            String boldFont = "C:/Users/yyy/AppData/Local/Microsoft/Windows/Fonts/SourceHanSansSC-Bold-2.otf";
+
+            // 注册字体并强制指定别名为 "思源黑体"
+            fontResolver.addFont(
+                    mediumFont,                // 字体文件路径
+                    "思源黑体",                 // fontFamilyNameOverride:覆盖默认字体名
+                    BaseFont.IDENTITY_H,       // 编码(必须用于中文)
+                    true,                      // 是否嵌入PDF
+                    null                       // PFB路径(仅AFM/PFM字体需要)
+            );
+
+            fontResolver.addFont(
+                    boldFont,
+                    "思源黑体 Medium",
+                    BaseFont.IDENTITY_H,
+                    true,
+                    null
+            );
+            html = html.replace("C:\\", "file:///C:/")
+                    .replace("\\", "/");
+            // 设置HTML(确保CSS中使用相同的font-family)
+            renderer.setDocumentFromString(html, "file:///");
+
+
+            // 渲染PDF
+            renderer.layout();
+            renderer.createPDF(os);
+        }
+    }
+}