feat: word文档调整暂存2
parent
58674642e7
commit
cd4b5365d6
|
|
@ -116,7 +116,7 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
|||
? applyPdfWatermark(baseBytes, resolveWatermarkText(loginUser))
|
||||
: baseBytes;
|
||||
|
||||
return new MeetingSummaryExportResult(bytes, contentType, safeTitle + "-AI-Summary." + ext);
|
||||
return new MeetingSummaryExportResult(bytes, contentType, safeTitle + "." + ext);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException("导出失败:" + ex.getMessage(), ex);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
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.XWPFParagraph;
|
||||
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.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
|
|
@ -15,7 +24,20 @@ import java.util.regex.Pattern;
|
|||
final class MeetingWordDocumentBuilder {
|
||||
|
||||
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(
|
||||
"(?<=\\p{IsHan})[\\h\\u200B]+(?=[\\p{IsLatin}\\p{N}])"
|
||||
+ "|(?<=[\\p{IsLatin}\\p{N}])[\\h\\u200B]+(?=\\p{IsHan})"
|
||||
|
|
@ -26,46 +48,55 @@ final class MeetingWordDocumentBuilder {
|
|||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
document.getProperties().getCustomProperties()
|
||||
.addProperty(EXPORT_VERSION_PROPERTY, EXPORT_VERSION);
|
||||
applyPageLayout(document);
|
||||
|
||||
XWPFParagraph title = document.createParagraph();
|
||||
title.setAlignment(ParagraphAlignment.CENTER);
|
||||
title.setSpacingAfter(240);
|
||||
XWPFRun titleRun = title.createRun();
|
||||
titleRun.setBold(true);
|
||||
titleRun.setFontSize(16);
|
||||
titleRun.setText((meeting.getTitle() == null ? "Meeting" : meeting.getTitle()) + " - AI Summary");
|
||||
titleRun.setFontSize(22);
|
||||
titleRun.setText(meeting.getTitle() == null ? "Meeting" : meeting.getTitle());
|
||||
|
||||
XWPFParagraph timeP = document.createParagraph();
|
||||
timeP.createRun().setText("Meeting Time: " + String.valueOf(meeting.getMeetingTime()));
|
||||
appendMetadata(timeP, "会议时间:" + formatMeetingTime(meeting));
|
||||
|
||||
XWPFParagraph hostP = document.createParagraph();
|
||||
hostP.createRun().setText("Host: " + (meeting.getHostName() == null ? "" : meeting.getHostName()));
|
||||
|
||||
XWPFParagraph participantsP = document.createParagraph();
|
||||
participantsP.createRun().setText("Participants: " + (meeting.getParticipants() == null ? "" : meeting.getParticipants()));
|
||||
|
||||
document.createParagraph();
|
||||
XWPFParagraph separator = document.createParagraph();
|
||||
separator.setSpacingAfter(120);
|
||||
|
||||
String summaryContent = normalizeMixedContentSpacing(meeting.getSummaryContent());
|
||||
for (MdBlock block : parseMarkdownBlocks(summaryContent)) {
|
||||
XWPFParagraph paragraph = document.createParagraph();
|
||||
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);
|
||||
} 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) {
|
||||
paragraph.setIndentationLeft(360);
|
||||
XWPFRun bullet = paragraph.createRun();
|
||||
bullet.setFontSize(12);
|
||||
bullet.setText("- ");
|
||||
appendMarkdownRuns(paragraph, block.text, false, 12);
|
||||
applyBodyParagraphStyle(paragraph);
|
||||
appendMarkdownRuns(paragraph, block.text, false, BODY_FONT_SIZE);
|
||||
} else {
|
||||
appendMarkdownRuns(paragraph, block.text, false, 12);
|
||||
applyBodyParagraphStyle(paragraph);
|
||||
appendMarkdownRuns(paragraph, block.text, false, BODY_FONT_SIZE);
|
||||
}
|
||||
}
|
||||
appendGeneratedByFooter(document);
|
||||
|
||||
applyDocumentFont(document);
|
||||
document.write(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private String formatMeetingTime(MeetingVO meeting) {
|
||||
return meeting.getMeetingTime() == null ? "" : meeting.getMeetingTime().format(MEETING_TIME_FORMATTER);
|
||||
}
|
||||
|
||||
private String normalizeMixedContentSpacing(String markdown) {
|
||||
if (markdown == null) {
|
||||
return null;
|
||||
|
|
@ -73,6 +104,119 @@ final class MeetingWordDocumentBuilder {
|
|||
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) {
|
||||
String input = text == null ? "" : text;
|
||||
Matcher matcher = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
|
||||
|
|
@ -118,6 +262,10 @@ final class MeetingWordDocumentBuilder {
|
|||
flushParagraph(blocks, paragraph);
|
||||
continue;
|
||||
}
|
||||
if (HORIZONTAL_RULE.matcher(line).matches()) {
|
||||
flushParagraph(blocks, paragraph);
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("#")) {
|
||||
flushParagraph(blocks, paragraph);
|
||||
int level = 0;
|
||||
|
|
@ -128,6 +276,11 @@ final class MeetingWordDocumentBuilder {
|
|||
blocks.add(new MdBlock(MdType.HEADING, level, line.substring(level).trim()));
|
||||
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("* ")) {
|
||||
flushParagraph(blocks, paragraph);
|
||||
blocks.add(new MdBlock(MdType.LIST, 0, line.substring(2).trim()));
|
||||
|
|
@ -170,6 +323,7 @@ final class MeetingWordDocumentBuilder {
|
|||
|
||||
private enum MdType {
|
||||
HEADING,
|
||||
QUOTE,
|
||||
LIST,
|
||||
PARAGRAPH
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue