feat: PDF文档调整
parent
cd4b5365d6
commit
c8df0d9604
|
|
@ -17,6 +17,7 @@ import lombok.RequiredArgsConstructor;
|
|||
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||
|
|
@ -29,6 +30,8 @@ import org.commonmark.node.Node;
|
|||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.renderer.html.HtmlRenderer;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.parser.Tag;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -59,6 +62,8 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
|||
private static final float WATERMARK_LINE_GAP = 34f;
|
||||
private static final float WATERMARK_BASE_STEP_X = 430f;
|
||||
private static final float WATERMARK_BASE_STEP_Y = 300f;
|
||||
private static final String PDF_EXPORT_VERSION_PROPERTY = "iMeetingPdfExportVersion";
|
||||
private static final String PDF_EXPORT_VERSION = "3";
|
||||
|
||||
private final MeetingSummaryFileService meetingSummaryFileService;
|
||||
private final SysTenantUserService sysTenantUserService;
|
||||
|
|
@ -99,6 +104,9 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
|||
if (!needRegenerate && "docx".equals(ext) && !isCurrentWordExport(exportPath)) {
|
||||
needRegenerate = true;
|
||||
}
|
||||
if (!needRegenerate && isPdf && !isCurrentPdfExport(exportPath)) {
|
||||
needRegenerate = true;
|
||||
}
|
||||
|
||||
byte[] baseBytes;
|
||||
if (needRegenerate) {
|
||||
|
|
@ -136,45 +144,18 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isCurrentPdfExport(Path exportPath) {
|
||||
try (PDDocument document = PDDocument.load(exportPath.toFile())) {
|
||||
String version = document.getDocumentInformation()
|
||||
.getCustomMetadataValue(PDF_EXPORT_VERSION_PROPERTY);
|
||||
return PDF_EXPORT_VERSION.equals(version);
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] buildPdfBytes(MeetingVO meeting) throws IOException {
|
||||
Parser parser = Parser.builder().build();
|
||||
String markdown = meeting.getSummaryContent() == null ? "" : meeting.getSummaryContent();
|
||||
Node document = parser.parse(markdown);
|
||||
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||
String htmlBody = renderer.render(document);
|
||||
|
||||
String title = meeting.getTitle() == null ? "Meeting" : meeting.getTitle();
|
||||
String time = meeting.getMeetingTime() == null ? "" : meeting.getMeetingTime().toString();
|
||||
String host = meeting.getHostName() == null ? "" : meeting.getHostName();
|
||||
String participants = meeting.getParticipants() == null ? "" : meeting.getParticipants();
|
||||
|
||||
String html = "<html><head><style>" +
|
||||
"body { font-family: 'NotoSansSC', 'SimSun', sans-serif; padding: 20px; line-height: 1.6; color: #333; }" +
|
||||
"h1, h2, h3 { color: #1890ff; border-bottom: 1px solid #eee; padding-bottom: 5px; }" +
|
||||
"table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }" +
|
||||
"table, th, td { border: 1px solid #ddd; }" +
|
||||
"th, td { padding: 8px 12px; text-align: left; }" +
|
||||
"th { background-color: #f5f5f5; font-weight: bold; }" +
|
||||
"blockquote { padding: 8px 16px; color: #666; border-left: 4px solid #1890ff; background: #f0f7ff; margin: 0 0 16px 0; }" +
|
||||
"</style></head><body>" +
|
||||
"<div style='text-align:center; margin-bottom:30px; border-bottom: 2px solid #1890ff; padding-bottom:20px;'>" +
|
||||
"<h1 style='font-size:28px; margin-bottom:12px; color:#000; border:none;'>" + title + "</h1>" +
|
||||
"<div style='font-size:14px; color:#666;'>" +
|
||||
"<span>Meeting Time: " + time + "</span>" +
|
||||
"<span style='margin: 0 20px;'>|</span>" +
|
||||
"<span>Host: " + host + "</span>" +
|
||||
"<span style='margin: 0 20px;'>|</span>" +
|
||||
"<span>Participants: " + participants + "</span>" +
|
||||
"</div></div>" +
|
||||
"<div class='markdown-body'>" + htmlBody + "</div>" +
|
||||
"<div style='margin-top: 40px; text-align: right; font-size: 12px; color: #999; border-top: 1px dashed #eee; padding-top: 10px;'>" +
|
||||
"Generated by iMeeting AI Assistant" +
|
||||
"</div>" +
|
||||
"</body></html>";
|
||||
|
||||
org.jsoup.nodes.Document jsoupDoc = Jsoup.parse(html);
|
||||
jsoupDoc.outputSettings().syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml);
|
||||
String xhtml = jsoupDoc.html();
|
||||
String xhtml = buildPdfXhtml(meeting);
|
||||
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
PdfRendererBuilder builder = new PdfRendererBuilder();
|
||||
|
|
@ -204,12 +185,134 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
|||
builder.withHtmlContent(xhtml, null);
|
||||
builder.toStream(out);
|
||||
builder.run();
|
||||
return out.toByteArray();
|
||||
return markCurrentPdfExport(out.toByteArray());
|
||||
} catch (Exception ex) {
|
||||
throw new IOException("PDF 生成失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] markCurrentPdfExport(byte[] pdfBytes) throws IOException {
|
||||
try (PDDocument document = PDDocument.load(pdfBytes);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
PDDocumentInformation information = document.getDocumentInformation();
|
||||
information.setCustomMetadataValue(PDF_EXPORT_VERSION_PROPERTY, PDF_EXPORT_VERSION);
|
||||
document.setDocumentInformation(information);
|
||||
document.save(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
String buildPdfXhtml(MeetingVO meeting) {
|
||||
Parser parser = Parser.builder().build();
|
||||
String markdown = meeting.getSummaryContent() == null ? "" : meeting.getSummaryContent();
|
||||
Node document = parser.parse(markdown);
|
||||
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||
String htmlBody = renderer.render(document);
|
||||
|
||||
String title = meeting.getTitle() == null ? "Meeting" : meeting.getTitle();
|
||||
String time = meeting.getMeetingTime() == null ? "" : meeting.getMeetingTime().toString();
|
||||
String host = meeting.getHostName() == null ? "" : meeting.getHostName();
|
||||
String participants = meeting.getParticipants() == null ? "" : meeting.getParticipants();
|
||||
|
||||
String html = "<html><head><style>" +
|
||||
"body { font-family: 'NotoSansSC', 'SimSun', sans-serif; padding: 20px; line-height: 1.6; color: #333; }" +
|
||||
"h1, h2, h3 { color: #1890ff; border-bottom: 1px solid #eee; padding-bottom: 5px; }" +
|
||||
".markdown-body { font-size: 14px; }" +
|
||||
".markdown-body p { margin: 0 0 8px 0; }" +
|
||||
".pdf-list-item { margin: 2px 0 4px 0; padding-left: 10px; text-indent: -10px; line-height: 1.6; }" +
|
||||
".pdf-list-level-1 { margin-left: 22px; }" +
|
||||
".pdf-list-level-2 { margin-left: 44px; }" +
|
||||
".pdf-list-level-3 { margin-left: 66px; }" +
|
||||
".pdf-list-marker { display: inline-block; width: 10px; font-size: 12px; line-height: 1; vertical-align: 0.06em; color: #333; }" +
|
||||
".pdf-list-content { display: inline; }" +
|
||||
"table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }" +
|
||||
"table, th, td { border: 1px solid #ddd; }" +
|
||||
"th, td { padding: 8px 12px; text-align: left; }" +
|
||||
"th { background-color: #f5f5f5; font-weight: bold; }" +
|
||||
"blockquote { padding: 8px 16px; color: #666; border-left: 4px solid #1890ff; background: #f0f7ff; margin: 0 0 16px 0; }" +
|
||||
"</style></head><body>" +
|
||||
"<div style='text-align:center; margin-bottom:30px; border-bottom: 2px solid #1890ff; padding-bottom:20px;'>" +
|
||||
"<h1 style='font-size:28px; margin-bottom:12px; color:#000; border:none;'>" + title + "</h1>" +
|
||||
"<div style='font-size:14px; color:#666;'>" +
|
||||
"<span>Meeting Time: " + time + "</span>" +
|
||||
"<span style='margin: 0 20px;'>|</span>" +
|
||||
"<span>Host: " + host + "</span>" +
|
||||
"<span style='margin: 0 20px;'>|</span>" +
|
||||
"<span>Participants: " + participants + "</span>" +
|
||||
"</div></div>" +
|
||||
"<div class='markdown-body'>" + htmlBody + "</div>" +
|
||||
"<div style='margin-top: 40px; text-align: right; font-size: 12px; color: #999; border-top: 1px dashed #eee; padding-top: 10px;'>" +
|
||||
"Generated by iMeeting AI Assistant" +
|
||||
"</div>" +
|
||||
"</body></html>";
|
||||
|
||||
org.jsoup.nodes.Document jsoupDoc = Jsoup.parse(html);
|
||||
normalizeMarkdownLists(jsoupDoc.selectFirst(".markdown-body"), 0);
|
||||
jsoupDoc.outputSettings().syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml);
|
||||
return jsoupDoc.html();
|
||||
}
|
||||
|
||||
private void normalizeMarkdownLists(Element container, int level) {
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
for (Element child : new ArrayList<>(container.children())) {
|
||||
if (isListElement(child)) {
|
||||
for (org.jsoup.nodes.Node node : toPdfListRows(child, level)) {
|
||||
child.before(node);
|
||||
}
|
||||
child.remove();
|
||||
} else {
|
||||
normalizeMarkdownLists(child, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<org.jsoup.nodes.Node> toPdfListRows(Element list, int level) {
|
||||
List<org.jsoup.nodes.Node> rows = new ArrayList<>();
|
||||
for (Element item : list.children()) {
|
||||
if (!"li".equalsIgnoreCase(item.tagName())) {
|
||||
continue;
|
||||
}
|
||||
Element row = new Element(Tag.valueOf("p"), "");
|
||||
row.addClass("pdf-list-item");
|
||||
row.addClass("pdf-list-level-" + Math.min(level, 3));
|
||||
|
||||
Element marker = new Element(Tag.valueOf("span"), "");
|
||||
marker.addClass("pdf-list-marker");
|
||||
marker.text("•");
|
||||
row.appendChild(marker);
|
||||
|
||||
Element content = new Element(Tag.valueOf("span"), "");
|
||||
content.addClass("pdf-list-content");
|
||||
|
||||
List<Element> nestedLists = new ArrayList<>();
|
||||
for (org.jsoup.nodes.Node node : new ArrayList<>(item.childNodes())) {
|
||||
if (node instanceof Element element && isListElement(element)) {
|
||||
nestedLists.add(element);
|
||||
} else if (node instanceof Element element && "p".equalsIgnoreCase(element.tagName())) {
|
||||
for (org.jsoup.nodes.Node paragraphNode : new ArrayList<>(element.childNodes())) {
|
||||
content.appendChild(paragraphNode);
|
||||
}
|
||||
element.remove();
|
||||
} else {
|
||||
content.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
row.appendChild(content);
|
||||
rows.add(row);
|
||||
for (Element nestedList : nestedLists) {
|
||||
rows.addAll(toPdfListRows(nestedList, level + 1));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private boolean isListElement(Element element) {
|
||||
return "ul".equalsIgnoreCase(element.tagName()) || "ol".equalsIgnoreCase(element.tagName());
|
||||
}
|
||||
|
||||
private byte[] applyPdfWatermark(byte[] pdfBytes, String watermarkText) throws IOException {
|
||||
if (watermarkText == null || watermarkText.isBlank()) {
|
||||
return pdfBytes;
|
||||
|
|
|
|||
Loading…
Reference in New Issue