feat: word文档调整暂存
parent
0aaa3a1cde
commit
58674642e7
|
|
@ -16,8 +16,6 @@ import com.unisbase.service.TenantManagementService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
|
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
|
||||||
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.XWPFRun;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
|
@ -38,6 +36,7 @@ import org.springframework.stereotype.Service;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
@ -48,8 +47,6 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
|
@ -99,12 +96,17 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
||||||
boolean isPdf = "pdf".equals(ext);
|
boolean isPdf = "pdf".equals(ext);
|
||||||
boolean needRegenerate = !Files.exists(exportPath)
|
boolean needRegenerate = !Files.exists(exportPath)
|
||||||
|| Files.getLastModifiedTime(exportPath).toMillis() < Files.getLastModifiedTime(summarySourcePath).toMillis();
|
|| Files.getLastModifiedTime(exportPath).toMillis() < Files.getLastModifiedTime(summarySourcePath).toMillis();
|
||||||
|
if (!needRegenerate && "docx".equals(ext) && !isCurrentWordExport(exportPath)) {
|
||||||
|
needRegenerate = true;
|
||||||
|
}
|
||||||
|
|
||||||
byte[] baseBytes;
|
byte[] baseBytes;
|
||||||
if (needRegenerate) {
|
if (needRegenerate) {
|
||||||
String markdown = Files.readString(summarySourcePath, StandardCharsets.UTF_8);
|
String markdown = Files.readString(summarySourcePath, StandardCharsets.UTF_8);
|
||||||
meetingDetail.setSummaryContent(meetingSummaryFileService.stripFrontMatter(markdown));
|
meetingDetail.setSummaryContent(meetingSummaryFileService.stripFrontMatter(markdown));
|
||||||
baseBytes = "docx".equals(ext) ? buildWordBytes(meetingDetail) : buildPdfBytes(meetingDetail);
|
baseBytes = "docx".equals(ext)
|
||||||
|
? new MeetingWordDocumentBuilder().build(meetingDetail)
|
||||||
|
: buildPdfBytes(meetingDetail);
|
||||||
Files.write(exportPath, baseBytes);
|
Files.write(exportPath, baseBytes);
|
||||||
} else {
|
} else {
|
||||||
baseBytes = Files.readAllBytes(exportPath);
|
baseBytes = Files.readAllBytes(exportPath);
|
||||||
|
|
@ -120,44 +122,17 @@ public class MeetingExportServiceImpl implements MeetingExportService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] buildWordBytes(MeetingVO meeting) throws IOException {
|
private boolean isCurrentWordExport(Path exportPath) {
|
||||||
try (XWPFDocument document = new XWPFDocument();
|
try (InputStream input = Files.newInputStream(exportPath);
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
XWPFDocument document = new XWPFDocument(input)) {
|
||||||
XWPFParagraph title = document.createParagraph();
|
var property = document.getProperties()
|
||||||
XWPFRun titleRun = title.createRun();
|
.getCustomProperties()
|
||||||
titleRun.setBold(true);
|
.getProperty(MeetingWordDocumentBuilder.EXPORT_VERSION_PROPERTY);
|
||||||
titleRun.setFontSize(16);
|
return property != null
|
||||||
titleRun.setText((meeting.getTitle() == null ? "Meeting" : meeting.getTitle()) + " - AI Summary");
|
&& property.isSetLpwstr()
|
||||||
|
&& MeetingWordDocumentBuilder.EXPORT_VERSION.equals(property.getLpwstr());
|
||||||
XWPFParagraph timeP = document.createParagraph();
|
} catch (Exception ignored) {
|
||||||
timeP.createRun().setText("Meeting Time: " + String.valueOf(meeting.getMeetingTime()));
|
return false;
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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