feat: word文档调整暂存2
parent
58674642e7
commit
cd4b5365d6
|
|
@ -116,7 +116,7 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
||||||
? applyPdfWatermark(baseBytes, resolveWatermarkText(loginUser))
|
? applyPdfWatermark(baseBytes, resolveWatermarkText(loginUser))
|
||||||
: baseBytes;
|
: baseBytes;
|
||||||
|
|
||||||
return new MeetingSummaryExportResult(bytes, contentType, safeTitle + "-AI-Summary." + ext);
|
return new MeetingSummaryExportResult(bytes, contentType, safeTitle + "." + ext);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new RuntimeException("导出失败:" + ex.getMessage(), ex);
|
throw new RuntimeException("导出失败:" + ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,21 @@
|
||||||
package com.imeeting.service.biz.impl;
|
package com.imeeting.service.biz.impl;
|
||||||
|
|
||||||
import com.imeeting.dto.biz.MeetingVO;
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
|
import org.apache.poi.xwpf.usermodel.LineSpacingRule;
|
||||||
|
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||||
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
|
||||||
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
|
||||||
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz;
|
||||||
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr;
|
||||||
|
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STPageOrientation;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
|
@ -15,7 +24,20 @@ import java.util.regex.Pattern;
|
||||||
final class MeetingWordDocumentBuilder {
|
final class MeetingWordDocumentBuilder {
|
||||||
|
|
||||||
static final String EXPORT_VERSION_PROPERTY = "iMeetingWordExportVersion";
|
static final String EXPORT_VERSION_PROPERTY = "iMeetingWordExportVersion";
|
||||||
static final String EXPORT_VERSION = "3";
|
static final String EXPORT_VERSION = "12";
|
||||||
|
private static final String DOCUMENT_FONT = "仿宋_GB2312";
|
||||||
|
private static final int BODY_FONT_SIZE = 14;
|
||||||
|
private static final int BODY_FIRST_LINE_INDENT = 640;
|
||||||
|
private static final int BODY_LINE_SPACING = 29;
|
||||||
|
private static final DateTimeFormatter MEETING_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||||
|
private static final Pattern BODY_STYLE_LIST_PREFIX = Pattern.compile(
|
||||||
|
"^\\*\\*([^*\\r\\n::]+?)(?:\\*\\*[\\h\\u200B]*([::])|([::])[\\h\\u200B]*\\*\\*)"
|
||||||
|
);
|
||||||
|
private static final Pattern FULLY_BOLD_LIST_ITEM = Pattern.compile(
|
||||||
|
"^\\*\\*([^*\\r\\n]+)\\*\\*$"
|
||||||
|
);
|
||||||
|
private static final Pattern QUOTE_PREFIX = Pattern.compile("^[>>]+[\\h\\u200B]*");
|
||||||
|
private static final Pattern HORIZONTAL_RULE = Pattern.compile("^[-*_](?:[\\h\\u200B]*[-*_]){2,}$");
|
||||||
private static final Pattern MIXED_CONTENT_SPACING = Pattern.compile(
|
private static final Pattern MIXED_CONTENT_SPACING = Pattern.compile(
|
||||||
"(?<=\\p{IsHan})[\\h\\u200B]+(?=[\\p{IsLatin}\\p{N}])"
|
"(?<=\\p{IsHan})[\\h\\u200B]+(?=[\\p{IsLatin}\\p{N}])"
|
||||||
+ "|(?<=[\\p{IsLatin}\\p{N}])[\\h\\u200B]+(?=\\p{IsHan})"
|
+ "|(?<=[\\p{IsLatin}\\p{N}])[\\h\\u200B]+(?=\\p{IsHan})"
|
||||||
|
|
@ -26,46 +48,55 @@ final class MeetingWordDocumentBuilder {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||||
document.getProperties().getCustomProperties()
|
document.getProperties().getCustomProperties()
|
||||||
.addProperty(EXPORT_VERSION_PROPERTY, EXPORT_VERSION);
|
.addProperty(EXPORT_VERSION_PROPERTY, EXPORT_VERSION);
|
||||||
|
applyPageLayout(document);
|
||||||
|
|
||||||
XWPFParagraph title = document.createParagraph();
|
XWPFParagraph title = document.createParagraph();
|
||||||
|
title.setAlignment(ParagraphAlignment.CENTER);
|
||||||
|
title.setSpacingAfter(240);
|
||||||
XWPFRun titleRun = title.createRun();
|
XWPFRun titleRun = title.createRun();
|
||||||
titleRun.setBold(true);
|
titleRun.setBold(true);
|
||||||
titleRun.setFontSize(16);
|
titleRun.setFontSize(22);
|
||||||
titleRun.setText((meeting.getTitle() == null ? "Meeting" : meeting.getTitle()) + " - AI Summary");
|
titleRun.setText(meeting.getTitle() == null ? "Meeting" : meeting.getTitle());
|
||||||
|
|
||||||
XWPFParagraph timeP = document.createParagraph();
|
XWPFParagraph timeP = document.createParagraph();
|
||||||
timeP.createRun().setText("Meeting Time: " + String.valueOf(meeting.getMeetingTime()));
|
appendMetadata(timeP, "会议时间:" + formatMeetingTime(meeting));
|
||||||
|
|
||||||
XWPFParagraph hostP = document.createParagraph();
|
XWPFParagraph separator = document.createParagraph();
|
||||||
hostP.createRun().setText("Host: " + (meeting.getHostName() == null ? "" : meeting.getHostName()));
|
separator.setSpacingAfter(120);
|
||||||
|
|
||||||
XWPFParagraph participantsP = document.createParagraph();
|
|
||||||
participantsP.createRun().setText("Participants: " + (meeting.getParticipants() == null ? "" : meeting.getParticipants()));
|
|
||||||
|
|
||||||
document.createParagraph();
|
|
||||||
|
|
||||||
String summaryContent = normalizeMixedContentSpacing(meeting.getSummaryContent());
|
String summaryContent = normalizeMixedContentSpacing(meeting.getSummaryContent());
|
||||||
for (MdBlock block : parseMarkdownBlocks(summaryContent)) {
|
for (MdBlock block : parseMarkdownBlocks(summaryContent)) {
|
||||||
XWPFParagraph paragraph = document.createParagraph();
|
XWPFParagraph paragraph = document.createParagraph();
|
||||||
if (block.type == MdType.HEADING) {
|
if (block.type == MdType.HEADING) {
|
||||||
int size = Math.max(12, 18 - (block.level - 1) * 2);
|
applyHeadingParagraphStyle(paragraph);
|
||||||
|
int size = Math.max(12, 16 - (block.level - 1) * 2);
|
||||||
appendMarkdownRuns(paragraph, block.text, true, size);
|
appendMarkdownRuns(paragraph, block.text, true, size);
|
||||||
|
} else if (block.type == MdType.QUOTE) {
|
||||||
|
applyBodyParagraphStyle(paragraph);
|
||||||
|
appendPlainText(paragraph, block.text, BODY_FONT_SIZE);
|
||||||
|
} else if (block.type == MdType.LIST && isBodyStyleListItem(block.text)) {
|
||||||
|
applyBodyParagraphStyle(paragraph);
|
||||||
|
appendMarkdownRuns(paragraph, demoteBodyStyleListPrefix(block.text), false, BODY_FONT_SIZE);
|
||||||
} else if (block.type == MdType.LIST) {
|
} else if (block.type == MdType.LIST) {
|
||||||
paragraph.setIndentationLeft(360);
|
applyBodyParagraphStyle(paragraph);
|
||||||
XWPFRun bullet = paragraph.createRun();
|
appendMarkdownRuns(paragraph, block.text, false, BODY_FONT_SIZE);
|
||||||
bullet.setFontSize(12);
|
|
||||||
bullet.setText("- ");
|
|
||||||
appendMarkdownRuns(paragraph, block.text, false, 12);
|
|
||||||
} else {
|
} else {
|
||||||
appendMarkdownRuns(paragraph, block.text, false, 12);
|
applyBodyParagraphStyle(paragraph);
|
||||||
|
appendMarkdownRuns(paragraph, block.text, false, BODY_FONT_SIZE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
appendGeneratedByFooter(document);
|
||||||
|
|
||||||
|
applyDocumentFont(document);
|
||||||
document.write(out);
|
document.write(out);
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String formatMeetingTime(MeetingVO meeting) {
|
||||||
|
return meeting.getMeetingTime() == null ? "" : meeting.getMeetingTime().format(MEETING_TIME_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizeMixedContentSpacing(String markdown) {
|
private String normalizeMixedContentSpacing(String markdown) {
|
||||||
if (markdown == null) {
|
if (markdown == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -73,6 +104,119 @@ final class MeetingWordDocumentBuilder {
|
||||||
return MIXED_CONTENT_SPACING.matcher(markdown).replaceAll("");
|
return MIXED_CONTENT_SPACING.matcher(markdown).replaceAll("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isBodyStyleListItem(String text) {
|
||||||
|
if (text == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Matcher prefixMatcher = BODY_STYLE_LIST_PREFIX.matcher(text);
|
||||||
|
if (prefixMatcher.find() && !text.substring(prefixMatcher.end()).isBlank()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Matcher fullBoldMatcher = FULLY_BOLD_LIST_ITEM.matcher(text);
|
||||||
|
if (!fullBoldMatcher.matches()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String plainText = fullBoldMatcher.group(1).trim();
|
||||||
|
int colonIndex = firstTitleColonIndex(plainText);
|
||||||
|
return colonIndex > 0 && colonIndex < plainText.length() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String demoteBodyStyleListPrefix(String text) {
|
||||||
|
return toPlainInline(text).trim()
|
||||||
|
.replaceFirst("[\\h\\u200B]*([::])[\\h\\u200B]*", "$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int firstTitleColonIndex(String text) {
|
||||||
|
for (int index = 0; index < text.length(); index++) {
|
||||||
|
char current = text.charAt(index);
|
||||||
|
if (current == ':') {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
if (current == ':'
|
||||||
|
&& (index == 0 || !Character.isDigit(text.charAt(index - 1)))
|
||||||
|
&& (index == text.length() - 1 || !Character.isDigit(text.charAt(index + 1)))) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBodyParagraphStyle(XWPFParagraph paragraph) {
|
||||||
|
paragraph.setAlignment(ParagraphAlignment.BOTH);
|
||||||
|
paragraph.setFirstLineIndent(BODY_FIRST_LINE_INDENT);
|
||||||
|
paragraph.setSpacingBetween(BODY_LINE_SPACING, LineSpacingRule.EXACT);
|
||||||
|
paragraph.setSpacingBefore(0);
|
||||||
|
paragraph.setSpacingAfter(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyHeadingParagraphStyle(XWPFParagraph paragraph) {
|
||||||
|
if (!paragraph.getCTP().isSetPPr()) {
|
||||||
|
paragraph.getCTP().addNewPPr();
|
||||||
|
}
|
||||||
|
paragraph.setKeepNext(true);
|
||||||
|
paragraph.setSpacingBefore(240);
|
||||||
|
paragraph.setSpacingAfter(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendMetadata(XWPFParagraph paragraph, String text) {
|
||||||
|
paragraph.setAlignment(ParagraphAlignment.RIGHT);
|
||||||
|
paragraph.setSpacingAfter(80);
|
||||||
|
XWPFRun run = paragraph.createRun();
|
||||||
|
run.setFontSize(12);
|
||||||
|
run.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendPlainText(XWPFParagraph paragraph, String text, int size) {
|
||||||
|
String plainText = toPlainInline(text);
|
||||||
|
if (plainText.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
XWPFRun run = paragraph.createRun();
|
||||||
|
run.setFontSize(size);
|
||||||
|
run.setText(plainText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendGeneratedByFooter(XWPFDocument document) {
|
||||||
|
XWPFParagraph paragraph = document.createParagraph();
|
||||||
|
paragraph.setAlignment(ParagraphAlignment.CENTER);
|
||||||
|
paragraph.setSpacingBefore(480);
|
||||||
|
paragraph.setSpacingAfter(0);
|
||||||
|
|
||||||
|
XWPFRun run = paragraph.createRun();
|
||||||
|
run.setItalic(true);
|
||||||
|
run.setFontSize(10);
|
||||||
|
run.setText("—— 内容由智听云AI生成 ——");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPageLayout(XWPFDocument document) {
|
||||||
|
CTBody body = document.getDocument().getBody();
|
||||||
|
CTSectPr section = body.isSetSectPr() ? body.getSectPr() : body.addNewSectPr();
|
||||||
|
CTPageSz pageSize = section.isSetPgSz() ? section.getPgSz() : section.addNewPgSz();
|
||||||
|
pageSize.setW(BigInteger.valueOf(11906));
|
||||||
|
pageSize.setH(BigInteger.valueOf(16838));
|
||||||
|
pageSize.setOrient(STPageOrientation.PORTRAIT);
|
||||||
|
|
||||||
|
CTPageMar margins = section.isSetPgMar() ? section.getPgMar() : section.addNewPgMar();
|
||||||
|
margins.setTop(BigInteger.valueOf(2098));
|
||||||
|
margins.setRight(BigInteger.valueOf(1531));
|
||||||
|
margins.setBottom(BigInteger.valueOf(1871));
|
||||||
|
margins.setLeft(BigInteger.valueOf(1531));
|
||||||
|
margins.setHeader(BigInteger.valueOf(708));
|
||||||
|
margins.setFooter(BigInteger.valueOf(708));
|
||||||
|
margins.setGutter(BigInteger.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyDocumentFont(XWPFDocument document) {
|
||||||
|
for (XWPFParagraph paragraph : document.getParagraphs()) {
|
||||||
|
for (XWPFRun run : paragraph.getRuns()) {
|
||||||
|
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.eastAsia);
|
||||||
|
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.ascii);
|
||||||
|
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.hAnsi);
|
||||||
|
run.setFontFamily(DOCUMENT_FONT, XWPFRun.FontCharRange.cs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void appendMarkdownRuns(XWPFParagraph paragraph, String text, boolean defaultBold, int size) {
|
private void appendMarkdownRuns(XWPFParagraph paragraph, String text, boolean defaultBold, int size) {
|
||||||
String input = text == null ? "" : text;
|
String input = text == null ? "" : text;
|
||||||
Matcher matcher = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
|
Matcher matcher = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
|
||||||
|
|
@ -118,6 +262,10 @@ final class MeetingWordDocumentBuilder {
|
||||||
flushParagraph(blocks, paragraph);
|
flushParagraph(blocks, paragraph);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (HORIZONTAL_RULE.matcher(line).matches()) {
|
||||||
|
flushParagraph(blocks, paragraph);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (line.startsWith("#")) {
|
if (line.startsWith("#")) {
|
||||||
flushParagraph(blocks, paragraph);
|
flushParagraph(blocks, paragraph);
|
||||||
int level = 0;
|
int level = 0;
|
||||||
|
|
@ -128,6 +276,11 @@ final class MeetingWordDocumentBuilder {
|
||||||
blocks.add(new MdBlock(MdType.HEADING, level, line.substring(level).trim()));
|
blocks.add(new MdBlock(MdType.HEADING, level, line.substring(level).trim()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (line.startsWith(">") || line.startsWith(">")) {
|
||||||
|
flushParagraph(blocks, paragraph);
|
||||||
|
blocks.add(new MdBlock(MdType.QUOTE, 0, QUOTE_PREFIX.matcher(line).replaceFirst("")));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (line.startsWith("- ") || line.startsWith("* ")) {
|
if (line.startsWith("- ") || line.startsWith("* ")) {
|
||||||
flushParagraph(blocks, paragraph);
|
flushParagraph(blocks, paragraph);
|
||||||
blocks.add(new MdBlock(MdType.LIST, 0, line.substring(2).trim()));
|
blocks.add(new MdBlock(MdType.LIST, 0, line.substring(2).trim()));
|
||||||
|
|
@ -170,6 +323,7 @@ final class MeetingWordDocumentBuilder {
|
||||||
|
|
||||||
private enum MdType {
|
private enum MdType {
|
||||||
HEADING,
|
HEADING,
|
||||||
|
QUOTE,
|
||||||
LIST,
|
LIST,
|
||||||
PARAGRAPH
|
PARAGRAPH
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue