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 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 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 copiedParts = new IdentityHashMap<>(); List sourceReferences = sourceSection.getEGHdrFtrReferences(); List 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 snapshotRelationships(Part source) { Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); return snapshotRelationships(source, visited); } private List snapshotRelationships(Part source, Set visited) { if (!visited.add(source)) { return new ArrayList<>(); } RelationshipsPart relationships = source.getRelationshipsPart(); if (relationships == null || relationships.getRelationships() == null) { return new ArrayList<>(); } List 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 relationships, Map 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 source, List 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 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 children; private RelatedPart(Relationship relationship, Part part, List 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); } } }