package com.trafficaudit.holiday.service;
|
|
import org.springframework.stereotype.Service;
|
import org.w3c.dom.Document;
|
import org.w3c.dom.Element;
|
import org.w3c.dom.Node;
|
import org.w3c.dom.NodeList;
|
|
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.transform.OutputKeys;
|
import javax.xml.transform.Transformer;
|
import javax.xml.transform.TransformerFactory;
|
import javax.xml.transform.dom.DOMSource;
|
import javax.xml.transform.stream.StreamResult;
|
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayOutputStream;
|
import java.io.InputStream;
|
import java.util.ArrayList;
|
import java.util.HashMap;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipOutputStream;
|
|
@Service
|
public class HolidayTrendWordService {
|
|
private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
private static final String DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/main";
|
private static final String WORD_DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
|
private static final String REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
private static final String PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
public byte[] generate(byte[] templateBytes, int year, List<DocumentBlock> blocks) {
|
try {
|
byte[] documentXml = readEntry(templateBytes, "word/document.xml");
|
Document document = parse(documentXml);
|
Element body = firstElement(document.getDocumentElement(), "body");
|
if (body == null) throw new IllegalArgumentException("走势图模板缺少正文");
|
|
TemplateParts parts = TemplateParts.from(document, body);
|
Map<String, byte[]> media = new LinkedHashMap<>();
|
List<ImageReference> imageReferences = new ArrayList<>();
|
int imageIndex = 1;
|
for (DocumentBlock block : blocks) {
|
if (!(block instanceof ChartBlock)) continue;
|
String imageName = "image" + imageIndex++ + ".png";
|
imageReferences.add(new ImageReference("rId" + (100 + imageIndex - 2),
|
"media/" + imageName, ((ChartBlock) block).getPng()));
|
media.put("word/media/" + imageName, ((ChartBlock) block).getPng());
|
}
|
|
Element sectPr = directChild(body, "sectPr");
|
List<Node> removals = directChildren(body);
|
for (Node node : removals) {
|
if (node != sectPr) body.removeChild(node);
|
}
|
|
appendClone(body, parts.titleLine, year + "年“十一”假期全省综合运输客流量", sectPr);
|
appendClone(body, parts.subtitleLine, null, sectPr);
|
appendClone(body, parts.titleBlank, null, sectPr);
|
|
int chartCursor = 0;
|
int chartsOnPage = 0;
|
for (int blockIndex = 0; blockIndex < blocks.size(); blockIndex++) {
|
DocumentBlock block = blocks.get(blockIndex);
|
if (block instanceof PageBreakBlock) {
|
appendPageBreak(body, sectPr);
|
chartsOnPage = 0;
|
} else if (block instanceof SectionBlock) {
|
appendClone(body, parts.sectionTemplate, ((SectionBlock) block).getText(), sectPr, true);
|
} else if (block instanceof ChartBlock) {
|
ChartBlock chart = (ChartBlock) block;
|
Element titleTemplate = chart.isCompact()
|
? parts.compactTitleTemplate : parts.lineTitleTemplate;
|
Element imageTemplate = chart.isCompact()
|
? parts.compactImageTemplate : parts.lineImageTemplate;
|
appendClone(body, titleTemplate, chart.getTitle(), sectPr);
|
Element imageParagraph = clone(imageTemplate);
|
ImageReference imageReference = imageReferences.get(chartCursor++);
|
bindImage(imageParagraph, imageReference, chart.getTitle(), chartCursor,
|
((ChartBlock) block).getImageSize());
|
body.insertBefore(imageParagraph, sectPr);
|
appendClone(body, parts.chartBlank, null, sectPr);
|
chartsOnPage++;
|
boolean firstChartPageBreak = chartCursor == 1
|
&& needsAutomaticPageBreak(blocks, blockIndex + 1);
|
if (firstChartPageBreak
|
|| chartsOnPage >= 2
|
&& needsAutomaticPageBreak(blocks, blockIndex + 1)) {
|
appendPageBreak(body, sectPr);
|
chartsOnPage = 0;
|
}
|
}
|
}
|
|
byte[] updatedDocument = serialize(document);
|
byte[] updatedRels = buildRelationships(templateBytes, imageReferences);
|
Map<String, byte[]> replacements = new HashMap<>();
|
replacements.put("word/document.xml", updatedDocument);
|
replacements.put("word/_rels/document.xml.rels", updatedRels);
|
replacements.putAll(media);
|
return rewriteZip(templateBytes, replacements);
|
} catch (Exception e) {
|
throw new IllegalArgumentException("走势图文档生成失败: " + e.getMessage(), e);
|
}
|
}
|
|
private byte[] buildRelationships(byte[] templateBytes, List<ImageReference> imageReferences) throws Exception {
|
Document rels = parse(readEntry(templateBytes, "word/_rels/document.xml.rels"));
|
Element root = rels.getDocumentElement();
|
NodeList children = root.getChildNodes();
|
List<Node> removals = new ArrayList<>();
|
for (int i = 0; i < children.getLength(); i++) {
|
Node node = children.item(i);
|
if (node instanceof Element) {
|
Element element = (Element) node;
|
if ("Relationship".equals(element.getLocalName())
|
&& element.getAttribute("Type").endsWith("/image")) {
|
removals.add(node);
|
}
|
}
|
}
|
for (Node node : removals) root.removeChild(node);
|
for (ImageReference reference : imageReferences) {
|
Element relationship = rels.createElementNS(PACKAGE_REL_NS, "Relationship");
|
relationship.setAttribute("Id", reference.getRelationshipId());
|
relationship.setAttribute("Type",
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image");
|
relationship.setAttribute("Target", reference.getTarget());
|
root.appendChild(relationship);
|
}
|
return serialize(rels);
|
}
|
|
private void bindImage(Element paragraph, ImageReference reference, String title, int index,
|
ImageSize imageSize) {
|
Element blip = firstElement(paragraph, "blip");
|
if (blip == null) throw new IllegalArgumentException("走势图模板图片节点缺失");
|
blip.setAttributeNS(REL_NS, "r:embed", reference.getRelationshipId());
|
Element srcRect = firstElement(paragraph, "srcRect");
|
if (srcRect != null) srcRect.getParentNode().removeChild(srcRect);
|
resizeDrawing(paragraph, imageSize);
|
Element docPr = firstElement(paragraph, "docPr");
|
if (docPr != null) {
|
docPr.setAttribute("id", String.valueOf(1000 + index));
|
docPr.setAttribute("name", title);
|
docPr.setAttribute("descr", title);
|
}
|
}
|
|
private void resizeDrawing(Element paragraph, ImageSize imageSize) {
|
if (imageSize == null) return;
|
NodeList extents = paragraph.getElementsByTagNameNS(WORD_DRAWING_NS, "extent");
|
for (int i = 0; i < extents.getLength(); i++) {
|
Element extent = (Element) extents.item(i);
|
extent.setAttribute("cx", String.valueOf(imageSize.getWidth()));
|
extent.setAttribute("cy", String.valueOf(imageSize.getHeight()));
|
}
|
NodeList transforms = paragraph.getElementsByTagNameNS(DRAWING_NS, "xfrm");
|
for (int i = 0; i < transforms.getLength(); i++) {
|
Element transform = (Element) transforms.item(i);
|
Element extent = firstElement(transform, "ext");
|
if (extent == null) continue;
|
extent.setAttribute("cx", String.valueOf(imageSize.getWidth()));
|
extent.setAttribute("cy", String.valueOf(imageSize.getHeight()));
|
}
|
}
|
|
private void appendClone(Element body, Element template, String text, Element before) {
|
appendClone(body, template, text, before, false);
|
}
|
|
private void appendClone(Element body, Element template, String text, Element before,
|
boolean removeNumbering) {
|
if (template == null) return;
|
Element copy = clone(template);
|
if (removeNumbering) removeNumbering(copy);
|
if (text != null) setParagraphText(copy, text);
|
body.insertBefore(copy, before);
|
}
|
|
private void removeNumbering(Element paragraph) {
|
NodeList numbered = paragraph.getElementsByTagNameNS(WORD_NS, "numPr");
|
while (numbered.getLength() > 0) {
|
Node node = numbered.item(0);
|
node.getParentNode().removeChild(node);
|
}
|
}
|
|
private boolean needsAutomaticPageBreak(List<DocumentBlock> blocks, int nextIndex) {
|
if (nextIndex >= blocks.size()) return false;
|
return !(blocks.get(nextIndex) instanceof PageBreakBlock);
|
}
|
|
private void appendPageBreak(Element body, Element before) {
|
Document document = body.getOwnerDocument();
|
Element paragraph = document.createElementNS(WORD_NS, "w:p");
|
Element run = document.createElementNS(WORD_NS, "w:r");
|
Element br = document.createElementNS(WORD_NS, "w:br");
|
br.setAttributeNS(WORD_NS, "w:type", "page");
|
run.appendChild(br);
|
paragraph.appendChild(run);
|
body.insertBefore(paragraph, before);
|
}
|
|
private void setParagraphText(Element paragraph, String text) {
|
Element pPr = firstDirectChild(paragraph, "pPr");
|
Element firstRun = firstDirectChild(paragraph, "r");
|
Element rPr = firstRun == null ? null : firstDirectChild(firstRun, "rPr");
|
List<Node> removals = new ArrayList<>();
|
Node child = paragraph.getFirstChild();
|
while (child != null) {
|
Node next = child.getNextSibling();
|
if (child != pPr) removals.add(child);
|
child = next;
|
}
|
for (Node node : removals) paragraph.removeChild(node);
|
Element run = paragraph.getOwnerDocument().createElementNS(WORD_NS, "w:r");
|
if (rPr != null) run.appendChild(rPr.cloneNode(true));
|
Element t = paragraph.getOwnerDocument().createElementNS(WORD_NS, "w:t");
|
t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve");
|
t.setTextContent(text);
|
run.appendChild(t);
|
paragraph.appendChild(run);
|
}
|
|
private Element clone(Element source) {
|
return (Element) source.cloneNode(true);
|
}
|
|
private byte[] rewriteZip(byte[] source, Map<String, byte[]> replacements) throws Exception {
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
Map<String, Boolean> seen = new HashMap<>();
|
try (ZipInputStream input = new ZipInputStream(new ByteArrayInputStream(source));
|
ZipOutputStream zipOutput = new ZipOutputStream(output)) {
|
ZipEntry entry;
|
byte[] buffer = new byte[8192];
|
while ((entry = input.getNextEntry()) != null) {
|
seen.put(entry.getName(), Boolean.TRUE);
|
byte[] replacement = replacements.get(entry.getName());
|
if (isGeneratedChartImage(entry.getName()) && replacement == null) continue;
|
zipOutput.putNextEntry(new ZipEntry(entry.getName()));
|
if (replacement != null) {
|
zipOutput.write(replacement);
|
} else {
|
int length;
|
while ((length = input.read(buffer)) > 0) {
|
zipOutput.write(buffer, 0, length);
|
}
|
}
|
zipOutput.closeEntry();
|
}
|
for (Map.Entry<String, byte[]> replacement : replacements.entrySet()) {
|
if (Boolean.TRUE.equals(seen.get(replacement.getKey()))) continue;
|
zipOutput.putNextEntry(new ZipEntry(replacement.getKey()));
|
zipOutput.write(replacement.getValue());
|
zipOutput.closeEntry();
|
}
|
}
|
return output.toByteArray();
|
}
|
|
private boolean isGeneratedChartImage(String entryName) {
|
return entryName != null && entryName.matches("word/media/image[0-9]+\\.png");
|
}
|
|
private byte[] readEntry(byte[] zipBytes, String entryName) throws Exception {
|
try (ZipInputStream input = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
ZipEntry entry;
|
while ((entry = input.getNextEntry()) != null) {
|
if (entryName.equals(entry.getName())) return readAll(input);
|
}
|
}
|
throw new IllegalArgumentException("模板中缺少 " + entryName);
|
}
|
|
private Document parse(byte[] xml) throws Exception {
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
factory.setNamespaceAware(true);
|
return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml));
|
}
|
|
private byte[] serialize(Document document) throws Exception {
|
Transformer transformer = TransformerFactory.newInstance().newTransformer();
|
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
transformer.transform(new DOMSource(document), new StreamResult(output));
|
return output.toByteArray();
|
}
|
|
private byte[] readAll(InputStream input) throws Exception {
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
byte[] buffer = new byte[8192];
|
int length;
|
while ((length = input.read(buffer)) > 0) {
|
output.write(buffer, 0, length);
|
}
|
return output.toByteArray();
|
}
|
|
private Element firstElement(Element parent, String localName) {
|
NodeList nodes = parent.getElementsByTagNameNS("*", localName);
|
if (nodes.getLength() > 0) return (Element) nodes.item(0);
|
if (DRAWING_NS.equals(parent.getNamespaceURI()) && parent.getLocalName() != null
|
&& localName.equals(parent.getLocalName())) return parent;
|
NodeList children = parent.getChildNodes();
|
for (int i = 0; i < children.getLength(); i++) {
|
Node node = children.item(i);
|
if (node instanceof Element) {
|
Element found = firstElement((Element) node, localName);
|
if (found != null) return found;
|
}
|
}
|
return null;
|
}
|
|
private Element firstDirectChild(Element parent, String localName) {
|
Node node = parent.getFirstChild();
|
while (node != null) {
|
if (node instanceof Element && localName.equals(localName(node))) return (Element) node;
|
node = node.getNextSibling();
|
}
|
return null;
|
}
|
|
private Element directChild(Element parent, String localName) {
|
return firstDirectChild(parent, localName);
|
}
|
|
private List<Node> directChildren(Element parent) {
|
List<Node> result = new ArrayList<>();
|
Node node = parent.getFirstChild();
|
while (node != null) {
|
result.add(node);
|
node = node.getNextSibling();
|
}
|
return result;
|
}
|
|
private String localName(Node node) {
|
return node.getLocalName() == null ? node.getNodeName().replaceFirst("^.*:", "") : node.getLocalName();
|
}
|
|
public static class DocumentBlock {
|
}
|
|
public static class SectionBlock extends DocumentBlock {
|
private final String text;
|
|
public SectionBlock(String text) {
|
this.text = text;
|
}
|
|
public String getText() {
|
return text;
|
}
|
}
|
|
public static class PageBreakBlock extends DocumentBlock {
|
}
|
|
public static class ChartBlock extends DocumentBlock {
|
private final String title;
|
private final byte[] png;
|
private final boolean compact;
|
private final ImageSize imageSize;
|
|
public ChartBlock(String title, byte[] png) {
|
this(title, png, false);
|
}
|
|
public ChartBlock(String title, byte[] png, boolean compact) {
|
this(title, png, compact, null);
|
}
|
|
public ChartBlock(String title, byte[] png, boolean compact, ImageSize imageSize) {
|
this.title = title;
|
this.png = png;
|
this.compact = compact;
|
this.imageSize = imageSize;
|
}
|
|
public ChartBlock(String title, byte[] png, ImageSize imageSize) {
|
this(title, png, false, imageSize);
|
}
|
|
public String getTitle() {
|
return title;
|
}
|
|
public byte[] getPng() {
|
return png;
|
}
|
|
public boolean isCompact() {
|
return compact;
|
}
|
|
public ImageSize getImageSize() {
|
return imageSize;
|
}
|
}
|
|
public static class ImageSize {
|
private final long width;
|
private final long height;
|
|
public ImageSize(long width, long height) {
|
this.width = width;
|
this.height = height;
|
}
|
|
public long getWidth() {
|
return width;
|
}
|
|
public long getHeight() {
|
return height;
|
}
|
}
|
|
private static class ImageReference {
|
private final String relationshipId;
|
private final String target;
|
private final byte[] image;
|
|
private ImageReference(String relationshipId, String target, byte[] image) {
|
this.relationshipId = relationshipId;
|
this.target = target;
|
this.image = image;
|
}
|
|
private String getRelationshipId() {
|
return relationshipId;
|
}
|
|
private String getTarget() {
|
return target;
|
}
|
|
private byte[] getImage() {
|
return image;
|
}
|
}
|
|
private static class TemplateParts {
|
private Element titleLine;
|
private Element subtitleLine;
|
private Element titleBlank;
|
private Element sectionTemplate;
|
private Element sectionBlank;
|
private Element lineTitleTemplate;
|
private Element lineImageTemplate;
|
private Element compactTitleTemplate;
|
private Element compactImageTemplate;
|
private Element chartBlank;
|
|
private static TemplateParts from(Document document, Element body) {
|
TemplateParts parts = new TemplateParts();
|
List<Element> paragraphs = directParagraphs(body);
|
parts.titleLine = byText(paragraphs, "2026年“十一”假期全省综合运输客流量");
|
parts.subtitleLine = byText(paragraphs, "走 势 图");
|
parts.sectionTemplate = byText(paragraphs, "总体情况");
|
parts.lineTitleTemplate = byText(paragraphs, "2026年“十一”假期全省跨区域人员流动量情况");
|
parts.lineImageTemplate = imageAfter(paragraphs, parts.lineTitleTemplate);
|
parts.compactTitleTemplate = byText(paragraphs, "“十一”假期各交通运输方式客运量占比情况");
|
parts.compactImageTemplate = imageAfter(paragraphs, parts.compactTitleTemplate);
|
parts.titleBlank = nextParagraph(paragraphs, parts.subtitleLine);
|
parts.sectionBlank = nextParagraph(paragraphs, parts.sectionTemplate);
|
parts.chartBlank = afterParagraph(paragraphs, parts.lineImageTemplate);
|
if (parts.titleLine == null || parts.subtitleLine == null || parts.sectionTemplate == null
|
|| parts.lineTitleTemplate == null || parts.lineImageTemplate == null
|
|| parts.compactTitleTemplate == null || parts.compactImageTemplate == null) {
|
throw new IllegalArgumentException("走势图模板结构不完整");
|
}
|
return parts;
|
}
|
|
private static List<Element> directParagraphs(Element body) {
|
List<Element> result = new ArrayList<>();
|
Node node = body.getFirstChild();
|
while (node != null) {
|
if (node instanceof Element && "p".equals(((Element) node).getLocalName())) {
|
result.add((Element) node);
|
}
|
node = node.getNextSibling();
|
}
|
return result;
|
}
|
|
private static Element byText(List<Element> paragraphs, String text) {
|
for (Element paragraph : paragraphs) {
|
if (text.equals(paragraphText(paragraph))) return paragraph;
|
}
|
return null;
|
}
|
|
private static Element imageAfter(List<Element> paragraphs, Element anchor) {
|
int index = paragraphs.indexOf(anchor);
|
if (index < 0) return null;
|
for (int i = index + 1; i < paragraphs.size(); i++) {
|
if (firstDescendant(paragraphs.get(i), "blip") != null) return paragraphs.get(i);
|
}
|
return null;
|
}
|
|
private static Element nextParagraph(List<Element> paragraphs, Element anchor) {
|
int index = paragraphs.indexOf(anchor);
|
return index >= 0 && index + 1 < paragraphs.size() ? paragraphs.get(index + 1) : null;
|
}
|
|
private static Element afterParagraph(List<Element> paragraphs, Element anchor) {
|
int index = paragraphs.indexOf(anchor);
|
return index >= 0 && index + 1 < paragraphs.size() ? paragraphs.get(index + 1) : null;
|
}
|
|
private static Element firstDescendant(Element parent, String localName) {
|
NodeList nodes = parent.getElementsByTagNameNS("*", localName);
|
return nodes.getLength() == 0 ? null : (Element) nodes.item(0);
|
}
|
|
private static String paragraphText(Element paragraph) {
|
StringBuilder builder = new StringBuilder();
|
NodeList nodes = paragraph.getElementsByTagNameNS(WORD_NS, "t");
|
for (int i = 0; i < nodes.getLength(); i++) {
|
builder.append(nodes.item(i).getTextContent());
|
}
|
return builder.toString();
|
}
|
}
|
}
|