|
@@ -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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|