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