package jnpf.fileStorage.dms;
|
|
import jnpf.exception.DataException;
|
import jnpf.service.DmsApprovalPageTagService;
|
import lombok.RequiredArgsConstructor;
|
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.docx4j.XmlUtils;
|
import org.docx4j.dml.wordprocessingDrawing.Inline;
|
import org.docx4j.model.structure.PageDimensions;
|
import org.docx4j.openpackaging.Base;
|
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
|
import org.docx4j.openpackaging.parts.Part;
|
import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage;
|
import org.docx4j.openpackaging.parts.WordprocessingML.FooterPart;
|
import org.docx4j.openpackaging.parts.WordprocessingML.HeaderPart;
|
import org.docx4j.openpackaging.parts.WordprocessingML.StyleDefinitionsPart;
|
import org.docx4j.openpackaging.parts.relationships.RelationshipsPart;
|
import org.docx4j.relationships.Relationship;
|
import org.docx4j.wml.Br;
|
import org.docx4j.wml.CTRel;
|
import org.docx4j.wml.Drawing;
|
import org.docx4j.wml.FooterReference;
|
import org.docx4j.wml.Hdr;
|
import org.docx4j.wml.HdrFtrRef;
|
import org.docx4j.wml.HeaderReference;
|
import org.docx4j.wml.Jc;
|
import org.docx4j.wml.JcEnumeration;
|
import org.docx4j.wml.ObjectFactory;
|
import org.docx4j.wml.P;
|
import org.docx4j.wml.PPr;
|
import org.docx4j.wml.R;
|
import org.docx4j.wml.SdtBlock;
|
import org.docx4j.wml.SdtContentBlock;
|
import org.docx4j.wml.SectPr;
|
import org.docx4j.wml.STBrType;
|
import org.docx4j.wml.Style;
|
import org.springframework.http.MediaType;
|
import org.springframework.stereotype.Service;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import javax.imageio.ImageIO;
|
import java.awt.image.BufferedImage;
|
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayOutputStream;
|
import java.io.File;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.nio.file.Files;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.HashSet;
|
import java.util.IdentityHashMap;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
import java.util.Set;
|
|
@Service
|
@RequiredArgsConstructor
|
public class DmsApprovalPageMergeService {
|
private static final ObjectFactory WML = new ObjectFactory();
|
private static final float PDF_DPI = 150F;
|
private static final long EMU_PER_TWIP = 635L;
|
private final DmsApprovalPageTagService approvalPageTagService;
|
|
public boolean hasApprovalPage(MultipartFile document) {
|
return approvalPageTagService.hasApprovalPage(document);
|
}
|
|
public MultipartFile prepend(MultipartFile document, MultipartFile approvalPage) {
|
String documentName = filename(document, "待登记文件");
|
if (!"docx".equals(extension(documentName))) {
|
throw new DataException("配置签批页时,主文件必须是 DOCX 格式");
|
}
|
if (hasApprovalPage(document)) return document;
|
return merge(document, approvalPage, false);
|
}
|
|
/** Replaces a previously generated approval page, or prepends one when absent. */
|
public MultipartFile replaceOrPrepend(MultipartFile document, MultipartFile approvalPage) {
|
String documentName = filename(document, "待登记文件");
|
if (!"docx".equals(extension(documentName))) {
|
throw new DataException("配置签批页时,主文件必须是 DOCX 格式");
|
}
|
return merge(document, approvalPage, true);
|
}
|
|
/** Drop the old generated page before filling original-document tags for a revision. */
|
public MultipartFile withoutApprovalPages(MultipartFile document) {
|
String documentName = filename(document, "待登记文件");
|
try {
|
WordprocessingMLPackage word = WordprocessingMLPackage.load(document.getInputStream());
|
if (approvalPageTagService.removeApprovalPages(word) == 0) return document;
|
return new GeneratedMultipartFile(document.getName(), documentName, save(word));
|
} catch (DataException e) {
|
throw e;
|
} catch (Exception e) {
|
throw new DataException("移除旧签批页失败:" + message(e));
|
}
|
}
|
|
private MultipartFile merge(MultipartFile document, MultipartFile approvalPage, boolean replace) {
|
String documentName = filename(document, "待登记文件");
|
String approvalName = filename(approvalPage, "签批页");
|
try {
|
WordprocessingMLPackage word = WordprocessingMLPackage.load(document.getInputStream());
|
if (replace) approvalPageTagService.removeApprovalPages(word);
|
byte[] merged;
|
switch (extension(approvalName)) {
|
case "pdf":
|
merged = prependPdf(word, approvalPage.getBytes());
|
break;
|
case "docx":
|
merged = prependDocx(word, approvalPage.getBytes());
|
break;
|
default:
|
throw new DataException("签批页仅支持 PDF 或 DOCX 格式");
|
}
|
return new GeneratedMultipartFile(document.getName(), documentName, merged);
|
} catch (DataException exception) {
|
throw exception;
|
} catch (Exception exception) {
|
throw new DataException("插入签批页失败:" + message(exception));
|
}
|
}
|
|
private byte[] prependPdf(WordprocessingMLPackage word, byte[] approvalPage) throws Exception {
|
List<Object> prefix = new ArrayList<>();
|
try (PDDocument pdf = Loader.loadPDF(approvalPage)) {
|
if (pdf.getNumberOfPages() == 0) throw new DataException("签批页 PDF 没有可用页面");
|
PDFRenderer renderer = new PDFRenderer(pdf);
|
PageDimensions page = pageDimensions(word);
|
long maxWidth = page.getWritableWidthTwips() * EMU_PER_TWIP;
|
long maxHeight = Math.max(1, page.getWritableHeightTwips() - 240L) * EMU_PER_TWIP;
|
for (int index = 0; index < pdf.getNumberOfPages(); index++) {
|
BufferedImage image = renderer.renderImageWithDPI(index, PDF_DPI, ImageType.RGB);
|
ByteArrayOutputStream png = new ByteArrayOutputStream();
|
if (!ImageIO.write(image, "png", png)) throw new DataException("签批页 PDF 无法转换为图片");
|
BinaryPartAbstractImage imagePart = BinaryPartAbstractImage.createImagePart(word, png.toByteArray());
|
long[] size = fit(image.getWidth(), image.getHeight(), maxWidth, maxHeight);
|
Inline inline = imagePart.createImageInline("approval-page-" + (index + 1),
|
"签批页 " + (index + 1), 1_000_000L + index, 1_000_000 + index,
|
size[0], size[1], false);
|
prefix.add(imageParagraph(inline));
|
prefix.add(pageBreak());
|
}
|
}
|
word.getMainDocumentPart().getContent().add(0, approvalPageTagService.taggedBlock(prefix));
|
return save(word);
|
}
|
|
private byte[] prependDocx(WordprocessingMLPackage word, byte[] approvalPage) throws Exception {
|
WordprocessingMLPackage approval = WordprocessingMLPackage.load(
|
new ByteArrayInputStream(approvalPage));
|
copyMissingStyles(word, approval);
|
|
List<Object> approvalContent = new ArrayList<>();
|
copyContent(approval.getMainDocumentPart().getContent(), approvalContent);
|
if (approvalContent.isEmpty()) {
|
throw new DataException("签批页 DOCX 没有可用内容");
|
}
|
|
SectPr approvalSection = copyApprovalSection(word, approval);
|
preventHeaderFooterInheritance(word, approvalSection);
|
SdtBlock prefix = approvalPageTagService.taggedBlock(approvalContent);
|
SdtContentBlock content = (SdtContentBlock) prefix.getSdtContent();
|
content.getContent().add(sectionBreak(approvalSection));
|
word.getMainDocumentPart().getContent().add(0, prefix);
|
return save(word);
|
}
|
|
private SectPr copyApprovalSection(WordprocessingMLPackage target,
|
WordprocessingMLPackage source) throws Exception {
|
SectPr sourceSection = source.getMainDocumentPart().getJaxbElement().getBody().getSectPr();
|
if (sourceSection == null) {
|
throw new DataException("签批页 DOCX 缺少页面设置");
|
}
|
SectPr result = XmlUtils.deepCopy(sourceSection);
|
SectPr.Type type = WML.createSectPrType();
|
type.setVal("nextPage");
|
result.setType(type);
|
result.setPrinterSettings(null);
|
|
Map<Part, Part> copiedParts = new IdentityHashMap<>();
|
List<CTRel> sourceReferences = sourceSection.getEGHdrFtrReferences();
|
List<CTRel> targetReferences = result.getEGHdrFtrReferences();
|
for (int index = 0; index < targetReferences.size(); index++) {
|
CTRel sourceReference = sourceReferences.get(index);
|
CTRel targetReference = targetReferences.get(index);
|
Relationship sourceRelationship = source.getMainDocumentPart().getRelationshipsPart()
|
.getRelationshipByID(sourceReference.getId());
|
Part sourcePart = sourceRelationship == null ? null
|
: source.getMainDocumentPart().getRelationshipsPart().getPart(sourceRelationship);
|
if (sourcePart == null) {
|
throw new DataException("签批页页眉或页脚引用无效");
|
}
|
Part copiedPart = copiedParts.get(sourcePart);
|
Relationship targetRelationship;
|
if (copiedPart == null) {
|
copiedPart = copyHeaderFooterPart(sourcePart);
|
targetRelationship = target.getMainDocumentPart().addTargetPart(copiedPart,
|
RelationshipsPart.AddPartBehaviour.RENAME_IF_NAME_EXISTS);
|
copiedParts.put(sourcePart, copiedPart);
|
copyRelatedParts(copiedPart, snapshotRelationships(sourcePart), copiedParts);
|
} else {
|
targetRelationship = target.getMainDocumentPart().addTargetPart(copiedPart,
|
RelationshipsPart.AddPartBehaviour.REUSE_EXISTING);
|
}
|
targetReference.setId(targetRelationship.getId());
|
}
|
return result;
|
}
|
|
private void preventHeaderFooterInheritance(WordprocessingMLPackage target,
|
SectPr approvalSection) throws Exception {
|
if (target.getDocumentModel().getSections().isEmpty()) {
|
return;
|
}
|
SectPr followingSection = target.getDocumentModel().getSections().get(0).getSectPr();
|
if (followingSection == null) {
|
return;
|
}
|
for (CTRel approvalReference : approvalSection.getEGHdrFtrReferences()) {
|
if (hasReference(followingSection, approvalReference)) {
|
continue;
|
}
|
if (approvalReference instanceof HeaderReference) {
|
HeaderPart blank = new HeaderPart();
|
Hdr header = WML.createHdr();
|
header.getContent().add(WML.createP());
|
blank.setJaxbElement(header);
|
Relationship relationship = target.getMainDocumentPart().addTargetPart(blank,
|
RelationshipsPart.AddPartBehaviour.RENAME_IF_NAME_EXISTS);
|
HeaderReference reference = WML.createHeaderReference();
|
reference.setType(((HeaderReference) approvalReference).getType());
|
reference.setId(relationship.getId());
|
followingSection.getEGHdrFtrReferences().add(reference);
|
} else if (approvalReference instanceof FooterReference) {
|
FooterPart blank = new FooterPart();
|
org.docx4j.wml.Ftr footer = WML.createFtr();
|
footer.getContent().add(WML.createP());
|
blank.setJaxbElement(footer);
|
Relationship relationship = target.getMainDocumentPart().addTargetPart(blank,
|
RelationshipsPart.AddPartBehaviour.RENAME_IF_NAME_EXISTS);
|
FooterReference reference = WML.createFooterReference();
|
reference.setType(((FooterReference) approvalReference).getType());
|
reference.setId(relationship.getId());
|
followingSection.getEGHdrFtrReferences().add(reference);
|
}
|
}
|
}
|
|
private boolean hasReference(SectPr section, CTRel expected) {
|
for (CTRel reference : section.getEGHdrFtrReferences()) {
|
if (reference.getClass().equals(expected.getClass())
|
&& referenceType(reference) == referenceType(expected)) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
private HdrFtrRef referenceType(CTRel reference) {
|
if (reference instanceof HeaderReference) {
|
return ((HeaderReference) reference).getType();
|
}
|
if (reference instanceof FooterReference) {
|
return ((FooterReference) reference).getType();
|
}
|
return null;
|
}
|
|
private Part copyHeaderFooterPart(Part source) throws Exception {
|
if (source instanceof HeaderPart) {
|
HeaderPart result = new HeaderPart();
|
result.setJaxbElement(XmlUtils.deepCopy(((HeaderPart) source).getJaxbElement()));
|
return result;
|
}
|
if (source instanceof FooterPart) {
|
FooterPart result = new FooterPart();
|
result.setJaxbElement(XmlUtils.deepCopy(((FooterPart) source).getJaxbElement()));
|
return result;
|
}
|
throw new DataException("签批页分节包含不支持的页眉或页脚类型");
|
}
|
|
private List<RelatedPart> snapshotRelationships(Part source) {
|
Set<Part> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
return snapshotRelationships(source, visited);
|
}
|
|
private List<RelatedPart> snapshotRelationships(Part source, Set<Part> visited) {
|
if (!visited.add(source)) {
|
return new ArrayList<>();
|
}
|
RelationshipsPart relationships = source.getRelationshipsPart();
|
if (relationships == null || relationships.getRelationships() == null) {
|
return new ArrayList<>();
|
}
|
List<RelatedPart> result = new ArrayList<>();
|
for (Relationship relationship : relationships.getRelationships().getRelationship()) {
|
Part part = "External".equalsIgnoreCase(relationship.getTargetMode())
|
? null : relationships.getPart(relationship);
|
result.add(new RelatedPart(relationship, part,
|
part == null ? new ArrayList<>() : snapshotRelationships(part, visited)));
|
}
|
return result;
|
}
|
|
private void copyRelatedParts(Base target, List<RelatedPart> relationships,
|
Map<Part, Part> copiedParts) throws Exception {
|
for (RelatedPart related : relationships) {
|
Relationship sourceRelationship = related.relationship;
|
if (related.part == null) {
|
Relationship external = new org.docx4j.relationships.ObjectFactory()
|
.createRelationship();
|
external.setId(sourceRelationship.getId());
|
external.setType(sourceRelationship.getType());
|
external.setTarget(sourceRelationship.getTarget());
|
external.setTargetMode(sourceRelationship.getTargetMode());
|
target.getRelationshipsPart(true).addRelationship(external);
|
continue;
|
}
|
Part copied = copiedParts.get(related.part);
|
RelationshipsPart.AddPartBehaviour behaviour;
|
if (copied == null) {
|
copied = related.part;
|
copied.setRelationships(null);
|
copiedParts.put(related.part, copied);
|
behaviour = RelationshipsPart.AddPartBehaviour.RENAME_IF_NAME_EXISTS;
|
} else {
|
behaviour = RelationshipsPart.AddPartBehaviour.REUSE_EXISTING;
|
}
|
target.addTargetPart(copied, behaviour, sourceRelationship.getId());
|
if (behaviour == RelationshipsPart.AddPartBehaviour.RENAME_IF_NAME_EXISTS) {
|
copyRelatedParts(copied, related.children, copiedParts);
|
}
|
}
|
}
|
|
private void copyContent(List<Object> source, List<Object> target) {
|
for (Object value : source) {
|
target.add(XmlUtils.deepCopy(value));
|
}
|
}
|
|
private void copyMissingStyles(WordprocessingMLPackage target,
|
WordprocessingMLPackage source) throws Exception {
|
StyleDefinitionsPart sourcePart = source.getMainDocumentPart().getStyleDefinitionsPart();
|
if (sourcePart == null || sourcePart.getJaxbElement() == null) {
|
return;
|
}
|
StyleDefinitionsPart targetPart = target.getMainDocumentPart().getStyleDefinitionsPart(true);
|
Set<String> existing = new HashSet<>();
|
for (Style style : targetPart.getJaxbElement().getStyle()) {
|
if (style.getStyleId() != null) {
|
existing.add(style.getStyleId());
|
}
|
}
|
for (Style style : sourcePart.getJaxbElement().getStyle()) {
|
if (style.getStyleId() != null && existing.add(style.getStyleId())) {
|
targetPart.getJaxbElement().getStyle().add(XmlUtils.deepCopy(style));
|
}
|
}
|
}
|
|
private PageDimensions pageDimensions(WordprocessingMLPackage word) {
|
if (!word.getDocumentModel().getSections().isEmpty()) {
|
PageDimensions dimensions = word.getDocumentModel().getSections().get(0).getPageDimensions();
|
if (dimensions.getWritableWidthTwips() > 0 && dimensions.getWritableHeightTwips() > 0) {
|
return dimensions;
|
}
|
}
|
PageDimensions dimensions = new PageDimensions();
|
dimensions.setPgSize();
|
dimensions.setMargins();
|
return dimensions;
|
}
|
|
private P imageParagraph(Inline inline) {
|
Drawing drawing = WML.createDrawing();
|
drawing.getAnchorOrInline().add(inline);
|
R run = WML.createR();
|
run.getContent().add(drawing);
|
P paragraph = WML.createP();
|
PPr properties = WML.createPPr();
|
Jc center = WML.createJc();
|
center.setVal(JcEnumeration.CENTER);
|
properties.setJc(center);
|
paragraph.setPPr(properties);
|
paragraph.getContent().add(run);
|
return paragraph;
|
}
|
|
private P pageBreak() {
|
Br page = WML.createBr();
|
page.setType(STBrType.PAGE);
|
R run = WML.createR();
|
run.getContent().add(page);
|
P paragraph = WML.createP();
|
paragraph.getContent().add(run);
|
return paragraph;
|
}
|
|
private P sectionBreak(SectPr section) {
|
P paragraph = WML.createP();
|
PPr properties = WML.createPPr();
|
properties.setSectPr(section);
|
paragraph.setPPr(properties);
|
return paragraph;
|
}
|
|
private long[] fit(int width, int height, long maxWidth, long maxHeight) {
|
double scale = Math.min((double) maxWidth / width, (double) maxHeight / height);
|
return new long[]{Math.max(1, Math.round(width * scale)), Math.max(1, Math.round(height * scale))};
|
}
|
|
private byte[] save(WordprocessingMLPackage word) throws Exception {
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
word.save(output);
|
return output.toByteArray();
|
}
|
|
private String filename(MultipartFile file, String label) {
|
String name = file == null ? null : file.getOriginalFilename();
|
if (file == null || file.isEmpty() || name == null || name.isBlank()) {
|
throw new DataException(label + "为空或缺少文件名");
|
}
|
return name.trim();
|
}
|
|
private String extension(String filename) {
|
int separator = filename.lastIndexOf('.');
|
return separator < 0 ? "" : filename.substring(separator + 1).toLowerCase(Locale.ROOT);
|
}
|
|
private String message(Exception exception) {
|
String value = exception.getMessage();
|
return value == null || value.isBlank() ? exception.getClass().getSimpleName() : value;
|
}
|
|
private static final class RelatedPart {
|
private final Relationship relationship;
|
private final Part part;
|
private final List<RelatedPart> children;
|
|
private RelatedPart(Relationship relationship, Part part, List<RelatedPart> children) {
|
this.relationship = relationship;
|
this.part = part;
|
this.children = children;
|
}
|
}
|
|
private static final class GeneratedMultipartFile implements MultipartFile {
|
private final String name;
|
private final String originalFilename;
|
private final byte[] content;
|
|
private GeneratedMultipartFile(String name, String originalFilename, byte[] content) {
|
this.name = name;
|
this.originalFilename = originalFilename;
|
this.content = content;
|
}
|
|
@Override public String getName() { return name; }
|
@Override public String getOriginalFilename() { return originalFilename; }
|
@Override public String getContentType() { return MediaType.parseMediaType(
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document").toString(); }
|
@Override public boolean isEmpty() { return content.length == 0; }
|
@Override public long getSize() { return content.length; }
|
@Override public byte[] getBytes() { return content.clone(); }
|
@Override public InputStream getInputStream() { return new ByteArrayInputStream(content); }
|
@Override public void transferTo(File destination) throws IOException { Files.write(destination.toPath(), content); }
|
}
|
}
|