feat: word文档调整暂存
parent
0aaa3a1cde
commit
58674642e7
|
|
@ -16,8 +16,6 @@ import com.unisbase.service.TenantManagementService;
|
|||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
|
|
@ -38,6 +36,7 @@ import org.springframework.stereotype.Service;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
|
@ -48,8 +47,6 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
|
|
@ -96,18 +93,23 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
|||
Files.createDirectories(exportDir);
|
||||
|
||||
Path exportPath = exportDir.resolve("summary." + ext);
|
||||
boolean isPdf = "pdf".equals(ext);
|
||||
boolean isPdf = "pdf".equals(ext);
|
||||
boolean needRegenerate = !Files.exists(exportPath)
|
||||
|| Files.getLastModifiedTime(exportPath).toMillis() < Files.getLastModifiedTime(summarySourcePath).toMillis();
|
||||
if (!needRegenerate && "docx".equals(ext) && !isCurrentWordExport(exportPath)) {
|
||||
needRegenerate = true;
|
||||
}
|
||||
|
||||
byte[] baseBytes;
|
||||
byte[] baseBytes;
|
||||
if (needRegenerate) {
|
||||
String markdown = Files.readString(summarySourcePath, StandardCharsets.UTF_8);
|
||||
meetingDetail.setSummaryContent(meetingSummaryFileService.stripFrontMatter(markdown));
|
||||
baseBytes = "docx".equals(ext) ? buildWordBytes(meetingDetail) : buildPdfBytes(meetingDetail);
|
||||
Files.write(exportPath, baseBytes);
|
||||
baseBytes = "docx".equals(ext)
|
||||
? new MeetingWordDocumentBuilder().build(meetingDetail)
|
||||
: buildPdfBytes(meetingDetail);
|
||||
Files.write(exportPath, baseBytes);
|
||||
} else {
|
||||
baseBytes = Files.readAllBytes(exportPath);
|
||||
baseBytes = Files.readAllBytes(exportPath);
|
||||
}
|
||||
|
||||
byte[] bytes = isPdf
|
||||
|
|
@ -120,44 +122,17 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
|||
}
|
||||
}
|
||||
|
||||
private byte[] buildWordBytes(MeetingVO meeting) throws IOException {
|
||||
try (XWPFDocument document = new XWPFDocument();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
XWPFParagraph title = document.createParagraph();
|
||||
XWPFRun titleRun = title.createRun();
|
||||
titleRun.setBold(true);
|
||||
titleRun.setFontSize(16);
|
||||
titleRun.setText((meeting.getTitle() == null ? "Meeting" : meeting.getTitle()) + " - AI Summary");
|
||||
|
||||
XWPFParagraph timeP = document.createParagraph();
|
||||
timeP.createRun().setText("Meeting Time: " + String.valueOf(meeting.getMeetingTime()));
|
||||
|
||||
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();
|
||||
|
||||
for (MdBlock block : parseMarkdownBlocks(meeting.getSummaryContent())) {
|
||||
XWPFParagraph p = document.createParagraph();
|
||||
if (block.type == MdType.HEADING) {
|
||||
int size = Math.max(12, 18 - (block.level - 1) * 2);
|
||||
appendMarkdownRuns(p, block.text, true, size);
|
||||
} else if (block.type == MdType.LIST) {
|
||||
p.setIndentationLeft(360);
|
||||
XWPFRun bullet = p.createRun();
|
||||
bullet.setFontSize(12);
|
||||
bullet.setText("- ");
|
||||
appendMarkdownRuns(p, block.text, false, 12);
|
||||
} else {
|
||||
appendMarkdownRuns(p, block.text, false, 12);
|
||||
}
|
||||
}
|
||||
|
||||
document.write(out);
|
||||
return out.toByteArray();
|
||||
private boolean isCurrentWordExport(Path exportPath) {
|
||||
try (InputStream input = Files.newInputStream(exportPath);
|
||||
XWPFDocument document = new XWPFDocument(input)) {
|
||||
var property = document.getProperties()
|
||||
.getCustomProperties()
|
||||
.getProperty(MeetingWordDocumentBuilder.EXPORT_VERSION_PROPERTY);
|
||||
return property != null
|
||||
&& property.isSetLpwstr()
|
||||
&& MeetingWordDocumentBuilder.EXPORT_VERSION.equals(property.getLpwstr());
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -419,116 +394,4 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
|||
}
|
||||
}
|
||||
|
||||
private void appendMarkdownRuns(XWPFParagraph paragraph, String text, boolean defaultBold, int size) {
|
||||
String input = text == null ? "" : text;
|
||||
Matcher matcher = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
|
||||
int start = 0;
|
||||
while (matcher.find()) {
|
||||
String normal = toPlainInline(input.substring(start, matcher.start()));
|
||||
if (!normal.isEmpty()) {
|
||||
XWPFRun run = paragraph.createRun();
|
||||
run.setBold(defaultBold);
|
||||
run.setFontSize(size);
|
||||
run.setText(normal);
|
||||
}
|
||||
String boldText = toPlainInline(matcher.group(1));
|
||||
if (!boldText.isEmpty()) {
|
||||
XWPFRun run = paragraph.createRun();
|
||||
run.setBold(true);
|
||||
run.setFontSize(size);
|
||||
run.setText(boldText);
|
||||
}
|
||||
start = matcher.end();
|
||||
}
|
||||
String tail = toPlainInline(input.substring(start));
|
||||
if (!tail.isEmpty()) {
|
||||
XWPFRun run = paragraph.createRun();
|
||||
run.setBold(defaultBold);
|
||||
run.setFontSize(size);
|
||||
run.setText(tail);
|
||||
}
|
||||
}
|
||||
|
||||
private List<MdBlock> parseMarkdownBlocks(String markdown) {
|
||||
List<MdBlock> blocks = new ArrayList<>();
|
||||
if (markdown == null || markdown.trim().isEmpty()) {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
String[] lines = markdown.replace("\r\n", "\n").split("\n");
|
||||
StringBuilder paragraph = new StringBuilder();
|
||||
|
||||
for (String raw : lines) {
|
||||
String line = raw == null ? "" : raw.trim();
|
||||
if (line.isEmpty()) {
|
||||
flushParagraph(blocks, paragraph);
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("#")) {
|
||||
flushParagraph(blocks, paragraph);
|
||||
int level = 0;
|
||||
while (level < line.length() && line.charAt(level) == '#') {
|
||||
level++;
|
||||
}
|
||||
level = Math.min(level, 6);
|
||||
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.LIST, 0, line.substring(2).trim()));
|
||||
continue;
|
||||
}
|
||||
Matcher ordered = Pattern.compile("^\\d+\\.\\s+(.*)$").matcher(line);
|
||||
if (ordered.find()) {
|
||||
flushParagraph(blocks, paragraph);
|
||||
blocks.add(new MdBlock(MdType.LIST, 0, ordered.group(1).trim()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (paragraph.length() > 0) {
|
||||
paragraph.append(' ');
|
||||
}
|
||||
paragraph.append(line);
|
||||
}
|
||||
|
||||
flushParagraph(blocks, paragraph);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private void flushParagraph(List<MdBlock> blocks, StringBuilder paragraph) {
|
||||
if (paragraph.length() > 0) {
|
||||
blocks.add(new MdBlock(MdType.PARAGRAPH, 0, paragraph.toString()));
|
||||
paragraph.setLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
private String toPlainInline(String input) {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
return input
|
||||
.replaceAll("`([^`]+)`", "$1")
|
||||
.replaceAll("\\*\\*(.*?)\\*\\*", "$1")
|
||||
.replaceAll("\\*(.*?)\\*", "$1")
|
||||
.replaceAll("\\[(.*?)]\\((.*?)\\)", "$1");
|
||||
}
|
||||
|
||||
private enum MdType {
|
||||
HEADING,
|
||||
LIST,
|
||||
PARAGRAPH
|
||||
}
|
||||
|
||||
private static class MdBlock {
|
||||
private final MdType type;
|
||||
private final int level;
|
||||
private final String text;
|
||||
|
||||
private MdBlock(MdType type, int level, String text) {
|
||||
this.type = type;
|
||||
this.level = level;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
final class MeetingWordDocumentBuilder {
|
||||
|
||||
static final String EXPORT_VERSION_PROPERTY = "iMeetingWordExportVersion";
|
||||
static final String EXPORT_VERSION = "3";
|
||||
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})"
|
||||
);
|
||||
|
||||
byte[] build(MeetingVO meeting) throws IOException {
|
||||
try (XWPFDocument document = new XWPFDocument();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
document.getProperties().getCustomProperties()
|
||||
.addProperty(EXPORT_VERSION_PROPERTY, EXPORT_VERSION);
|
||||
|
||||
XWPFParagraph title = document.createParagraph();
|
||||
XWPFRun titleRun = title.createRun();
|
||||
titleRun.setBold(true);
|
||||
titleRun.setFontSize(16);
|
||||
titleRun.setText((meeting.getTitle() == null ? "Meeting" : meeting.getTitle()) + " - AI Summary");
|
||||
|
||||
XWPFParagraph timeP = document.createParagraph();
|
||||
timeP.createRun().setText("Meeting Time: " + String.valueOf(meeting.getMeetingTime()));
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
appendMarkdownRuns(paragraph, block.text, true, 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);
|
||||
} else {
|
||||
appendMarkdownRuns(paragraph, block.text, false, 12);
|
||||
}
|
||||
}
|
||||
|
||||
document.write(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeMixedContentSpacing(String markdown) {
|
||||
if (markdown == null) {
|
||||
return null;
|
||||
}
|
||||
return MIXED_CONTENT_SPACING.matcher(markdown).replaceAll("");
|
||||
}
|
||||
|
||||
private void appendMarkdownRuns(XWPFParagraph paragraph, String text, boolean defaultBold, int size) {
|
||||
String input = text == null ? "" : text;
|
||||
Matcher matcher = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
|
||||
int start = 0;
|
||||
while (matcher.find()) {
|
||||
String normal = toPlainInline(input.substring(start, matcher.start()));
|
||||
if (!normal.isEmpty()) {
|
||||
XWPFRun run = paragraph.createRun();
|
||||
run.setBold(defaultBold);
|
||||
run.setFontSize(size);
|
||||
run.setText(normal);
|
||||
}
|
||||
String boldText = toPlainInline(matcher.group(1));
|
||||
if (!boldText.isEmpty()) {
|
||||
XWPFRun run = paragraph.createRun();
|
||||
run.setBold(true);
|
||||
run.setFontSize(size);
|
||||
run.setText(boldText);
|
||||
}
|
||||
start = matcher.end();
|
||||
}
|
||||
String tail = toPlainInline(input.substring(start));
|
||||
if (!tail.isEmpty()) {
|
||||
XWPFRun run = paragraph.createRun();
|
||||
run.setBold(defaultBold);
|
||||
run.setFontSize(size);
|
||||
run.setText(tail);
|
||||
}
|
||||
}
|
||||
|
||||
private List<MdBlock> parseMarkdownBlocks(String markdown) {
|
||||
List<MdBlock> blocks = new ArrayList<>();
|
||||
if (markdown == null || markdown.trim().isEmpty()) {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
String[] lines = markdown.replace("\r\n", "\n").split("\n");
|
||||
StringBuilder paragraph = new StringBuilder();
|
||||
|
||||
for (String raw : lines) {
|
||||
String line = raw == null ? "" : raw.trim();
|
||||
if (line.isEmpty()) {
|
||||
flushParagraph(blocks, paragraph);
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("#")) {
|
||||
flushParagraph(blocks, paragraph);
|
||||
int level = 0;
|
||||
while (level < line.length() && line.charAt(level) == '#') {
|
||||
level++;
|
||||
}
|
||||
level = Math.min(level, 6);
|
||||
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.LIST, 0, line.substring(2).trim()));
|
||||
continue;
|
||||
}
|
||||
Matcher ordered = Pattern.compile("^\\d+\\.\\s+(.*)$").matcher(line);
|
||||
if (ordered.find()) {
|
||||
flushParagraph(blocks, paragraph);
|
||||
blocks.add(new MdBlock(MdType.LIST, 0, ordered.group(1).trim()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (paragraph.length() > 0) {
|
||||
paragraph.append(' ');
|
||||
}
|
||||
paragraph.append(line);
|
||||
}
|
||||
|
||||
flushParagraph(blocks, paragraph);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private void flushParagraph(List<MdBlock> blocks, StringBuilder paragraph) {
|
||||
if (paragraph.length() > 0) {
|
||||
blocks.add(new MdBlock(MdType.PARAGRAPH, 0, paragraph.toString()));
|
||||
paragraph.setLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
private String toPlainInline(String input) {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
return input
|
||||
.replaceAll("`([^`]+)`", "$1")
|
||||
.replaceAll("\\*\\*(.*?)\\*\\*", "$1")
|
||||
.replaceAll("\\*(.*?)\\*", "$1")
|
||||
.replaceAll("\\[(.*?)]\\((.*?)\\)", "$1");
|
||||
}
|
||||
|
||||
private enum MdType {
|
||||
HEADING,
|
||||
LIST,
|
||||
PARAGRAPH
|
||||
}
|
||||
|
||||
private static class MdBlock {
|
||||
private final MdType type;
|
||||
private final int level;
|
||||
private final String text;
|
||||
|
||||
private MdBlock(MdType type, int level, String text) {
|
||||
this.type = type;
|
||||
this.level = level;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue