PhpWord合并docx时如何复制样式与编号到目标文件
Word合并后项目符号编号显示异常修复
你的判断是对的:当前合并逻辑仅同步了正文内容、媒体资源、评论数据,缺失源文件的编号定义组件,导致正文里引用的编号规则找不到对应配置,才会出现显示异常。
生成文件正常效果
目标文件异常效果
以下是针对你贴的generate.php代码的具体修改方案:
核心问题原因
OOXML格式的docx文件中,所有列表、项目符号的编号规则都存在word/numbering.xml文件里,正文仅通过w:numId标签引用对应规则ID。你当前的代码没有处理这个文件,也没有补全这个文件对应的包关联、内容类型声明,Word打开时找不到编号定义就会渲染失败。
具体修改步骤
- 同步源文件的numbering.xml到目标包
现有遍历源压缩包复制资源的逻辑,仅匹配了media目录和comments.xml,新增numbering.xml的匹配规则即可:
如果你的模板文件本身已经自带numbering.xml(即模板预设了编号规则),不要直接覆盖,需要解析两个numbering.xml的内容,将源文件的numId、abstractNumId做偏移后合并到目标文件的numbering.xml中,避免ID冲突覆盖模板原有规则。空模板直接复制即可。// 原有遍历sourceZip文件的循环,修改判断条件 for ($i = 0; $i < $sourceZip->numFiles; $i++) { $currentFileName = $sourceZip->getNameIndex($i); // 新增numbering.xml的判断 if(strpos($currentFileName, 'media') !== false || strpos($currentFileName, 'comments.xml') !== false || strpos($currentFileName, 'numbering.xml') !== false){ if(strpos($currentFileName, 'comments.xml') !== false) $ifThereAreComments = 1; // 原有临时文件写入逻辑不变 $fileContent = $sourceZip->getFromName($currentFileName); $temp = fopen(TEMPORARYFILES.substr(bin2hex(random_bytes(5)),0, 5), "w"); fwrite($temp, $fileContent); fseek($temp, 0); $targetZip -> addFile(stream_get_meta_data($temp)['uri'], $currentFileName); } }- 同步源文件的numbering.xml到目标包
- 补全document.xml的关联关系
在现有添加评论关联、图片关联的逻辑段后,新增numbering.xml的关联声明,注意使用未被占用的关系ID:
// 放在if($ifThereAreComments)逻辑块之后即可 $targetRelationsDom = new DOMDocument(); $targetRelationsDom->loadXML($targetRelationsDocument); $relParentNode = $targetRelationsDom -> getElementsByTagName('Relationships')->item(0); // 新增numbering关系 $numberingRelNode = $targetRelationsDom->createElement("Relationship"); $idAttr = $targetRelationsDom->createAttribute('Id'); $idAttr->value = 'rId1001'; // 确保这个ID没被其他关系占用 $numberingRelNode->appendChild($idAttr); $targetAttr = $targetRelationsDom->createAttribute('Target'); $targetAttr->value = 'numbering.xml'; $numberingRelNode->appendChild($targetAttr); $typeAttr = $targetRelationsDom->createAttribute('Type'); $typeAttr->value = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering"; $numberingRelNode->appendChild($typeAttr); $relParentNode->appendChild($numberingRelNode); $targetZip->addFromString('word/_rels/document.xml.rels', $targetRelationsDom->saveXML());- 补全document.xml的关联关系
- 补全Content_Types声明
和comments.xml的类型声明逻辑一致,新增numbering.xml的内容类型:
$typeParentNode = $targetContentTypesDom -> getElementsByTagName('Types')->item(0); $numberingTypeNode = $targetContentTypesDom->createElement("Override"); $partAttr = $targetContentTypesDom->createAttribute('PartName'); $partAttr->value = '/word/numbering.xml'; $numberingTypeNode->appendChild($partAttr); $ctAttr = $targetContentTypesDom->createAttribute('ContentType'); $ctAttr->value = 'application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml'; $numberingTypeNode->appendChild($ctAttr); $typeParentNode->appendChild($numberingTypeNode); $targetZip->addFromString('[Content_Types].xml', $targetContentTypesDom->saveXML());- 补全Content_Types声明
扩展说明
如果后续合并后出现自定义样式显示异常,用完全相同的逻辑处理word/styles.xml即可:复制文件、补document.xml.rels关联、补Content_Types声明。如果模板自带styles.xml,同样需要做ID偏移合并,不要直接覆盖。
原有问题代码参考
if(!empty($newresult['header']) && !empty($result['snapshot'])) { \PhpOffice\PhpWord\Settings::setOutputEscapingEnabled(true); $phpWord = new \PhpOffice\PhpWord\PhpWord(); $section = $phpWord->addSection(); if(!empty($result['snapshot'])) { $decoded = json_decode($result['snapshot'], true); for($i = sizeof($decoded['blocks']) - 1; $i>= 0; $i--){ if($decoded['blocks'][$i]['text'] != '') break; else{ array_pop($decoded['blocks']); } } makeWordStrings($section, json_decode($newresult['snapshot'], true), $demographics, $decoded, $tabSize, 0); } $newfilename = $filename; if(mysqli_num_rows($query) > 1){ if($result['file_part_number'] > 1) $newfilename .= '_'.(strlen($result['file_part_number']-1)==1?'0'.($result['file_part_number']-1):($result['file_part_number']-1)); } $phpWord->save(WITHOUTTEMPLATE."$newfilename.docx", "Word2007"); $templateFile = TEMPLATES.$newresult['header']; $generatedFile = WITHOUTTEMPLATE."$newfilename.docx"; $targetFile = $storageDirectory.($includeNoName==1?"":"$newfilename.docx"); // copy template to target copy($templateFile, $targetFile); // open target $targetZip = new ZipArchive(); $targetZip->open($targetFile); $targetDocument = $targetZip->getFromName('word/document.xml'); $targetRelationsDocument = $targetZip->getFromName('word/_rels/document.xml.rels'); $targetCorePropsDocument = $targetZip->getFromName('docProps/core.xml'); $targetCustomPropsDocument = $targetZip->getFromName('docProps/custom.xml'); $targetContentTypesDocument = $targetZip->getFromName('[Content_Types].xml'); $targetDom = new DOMDocument(); $targetDom->loadXML($targetDocument); $targetCorePropsDom = new DOMDocument(); $targetCorePropsDom->loadXML($targetCorePropsDocument); $targetContentTypesDom = new DOMDocument(); $targetContentTypesDom->loadXML($targetContentTypesDocument); $targetXPath = new \DOMXPath($targetDom); $targetXPath->registerNamespace("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"); // open source $sourceZip = new ZipArchive(); $sourceZip->open($generatedFile); $sourceDocument = $sourceZip->getFromName('word/document.xml'); $sourceRelationsDocument = $sourceZip->getFromName('word/_rels/document.xml.rels'); $sourceContentTypesDocument = $sourceZip->getFromName('[Content_Types].xml'); $sourceDom = new DOMDocument(); $sourceDom->loadXML($sourceDocument); $sourceRelationsDom = new DOMDocument(); $sourceRelationsDom->loadXML($sourceRelationsDocument); $sourceContentTypes = new DOMDocument(); $sourceContentTypes->loadXML($sourceContentTypesDocument); $sourceXPath = new \DOMXPath($sourceDom); $sourceXPath->registerNamespace("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"); $sourceRelationsXPath = new \DOMXPath($sourceRelationsDom); $replacementMarkerNode = $targetXPath->query('//w:p[contains(translate(normalize-space(), " ", ""),"$CONTENT$")]')[0]; // insert source nodes before the replacement marker $sourceNodes = $sourceXPath->query('//w:document/w:body/*[not(self::w:sectPr)]'); $relationsArr = array(); $startId = 700; $ifThereAreComments = 0; for ($i = 0; $i < $sourceZip->numFiles; $i++) { if(strpos($sourceZip->getNameIndex($i), 'media') > 0 || strpos($sourceZip->getNameIndex($i), 'comments.xml') > 0){ if(strpos($sourceZip->getNameIndex($i), 'comments.xml') > 0) $ifThereAreComments = 1; $imagefile = $sourceZip->getFromName($sourceZip->getNameIndex($i)); $temp = fopen(TEMPORARYFILES.substr(bin2hex(random_bytes(5)),0, 5), "w"); fwrite($temp, $imagefile); fseek($temp, 0); $targetZip -> addFile(stream_get_meta_data($temp)['uri'], $sourceZip->getNameIndex($i)); } } foreach ($sourceNodes as $sourceNode) { $checkifImage = $sourceNode -> getElementsByTagNameNS ("urn:schemas-microsoft-com:vml", 'imagedata'); foreach($checkifImage AS $t) { $rid = $t->getAttribute('r:id'); $relationsArr[$rid] = array('id'=>'rId'.$startId, 'media'=>''); $t->setAttribute('r:id', $relationsArr[$rid]['id']); $startId++; } $imported = $replacementMarkerNode->ownerDocument->importNode($sourceNode, true); $inserted = $replacementMarkerNode->parentNode->insertBefore($imported, $replacementMarkerNode); } // remove $replacementMarkerNode from the target DOM $replacementMarkerNode->parentNode->removeChild($replacementMarkerNode); // save target $targetZip->addFromString('word/document.xml', $targetDom->saveXML()); $getSourceRelationsTree = $sourceRelationsDom -> getElementsByTagName('Relationship'); foreach($getSourceRelationsTree AS $t){ $rid = $t->getAttribute('Id'); if(isset($relationsArr[$rid])){ $relationsArr[$rid]['media'] = $t->getAttribute('Target'); } } if($ifThereAreComments){ $targetRelationsDom = new DOMDocument(); $targetRelationsDom->loadXML($targetRelationsDocument); $parentDom = $targetRelationsDom -> getElementsByTagName('Relationships'); $relationsElement = $targetRelationsDom->createElement("Relationship"); $domAttribute = $targetRelationsDom->createAttribute('Id'); $domAttribute->value = 'rId1000'; $relationsElement->appendChild($domAttribute); $domAttribute = $targetRelationsDom->createAttribute('Target'); $domAttribute->value = 'comments.xml'; $relationsElement->appendChild($domAttribute); $domAttribute = $targetRelationsDom->createAttribute('Type'); $domAttribute->value = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"; $relationsElement->appendChild($domAttribute); $parentDom[0]->appendChild($relationsElement); $targetZip->addFromString('word/_rels/document.xml.rels', $targetRelationsDom->saveXML()); $parentDom = $targetContentTypesDom -> getElementsByTagName('Types'); $relationsElement = $targetContentTypesDom->createElement("Override"); $domAttribute = $targetContentTypesDom->createAttribute('PartName'); $domAttribute->value = '/word/comments.xml'; $relationsElement->appendChild($domAttribute); $domAttribute = $targetContentTypesDom->createAttribute('ContentType'); $domAttribute->value = 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml'; $relationsElement->appendChild($domAttribute); $parentDom[0]->appendChild($relationsElement); $targetZip->addFromString('[Content_Types].xml', $targetContentTypesDom->saveXML()); } if(!empty($relationsArr)){ $targetRelationsDom = new DOMDocument(); $targetRelationsDom->loadXML($targetRelationsDocument); $parentDom = $targetRelationsDom -> getElementsByTagName('Relationships'); foreach($relationsArr AS $t){ $relationsElement = $targetRelationsDom->createElement("Relationship"); $domAttribute = $targetRelationsDom->createAttribute('Id'); $domAttribute->value = $t['id']; $relationsElement->appendChild($domAttribute); $domAttribute = $targetRelationsDom->createAttribute('Target'); $domAttribute->value = $t['media']; $relationsElement->appendChild($domAttribute); $domAttribute = $targetRelationsDom->createAttribute('Type'); $domAttribute->value = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"; $relationsElement->appendChild($domAttribute); $parentDom[0]->appendChild($relationsElement); } $targetZip->addFromString('word/_rels/document.xml.rels', $targetRelationsDom->saveXML()); } $parentDom = $targetCorePropsDom -> getElementsByTagNameNS("http://schemas.openxmlformats.org/package/2006/metadata/core-properties", 'coreProperties'); $toRem = $parentDom[0]->getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", 'title')->Item(0); $relationsElement = $targetCorePropsDom->createElement("dc:title", isset($demographics['{{title}}'])?$demographics['{{title}}']['value']:null); $parentDom[0]->appendChild($relationsElement); $toRem->parentNode->removeChild($toRem); $toRem = $parentDom[0]->getElementsByTagNameNS("http://purl.org/dc/elements/1.1/", 'subject')->Item(0); $relationsElement = $targetCorePropsDom->createElement("dc:subject", $dict_id); $parentDom[0]->appendChild($relationsElement); $toRem->parentNode->removeChild($toRem); $targetZip->addFromString('docProps/core.xml', $targetCorePropsDom->saveXML()); $targetZip->close(); }
内容的提问来源于stack exchange,提问作者Xohn
相关产品推荐
相关产品推荐

