PHP使用phpMergeDocx合并docx时所有图片被首份文件图片替换问题求助
问题原因
你现有的代码只合并了docx的正文XML内容,没有处理docx内置的媒体资源和关联映射,所以出现图片被覆盖的问题:
- docx是ZIP格式的压缩包,所有嵌入图片默认存放在
word/media/目录下,不同docx的图片命名都是image1.png、image2.jpg这类递增序列,直接合并会出现重名覆盖 - 正文XML中的图片通过
rId和word/_rels/document.xml.rels文件里的资源路径做关联,两个docx的rId序列也会重复,导致引用指向错误的资源
修复实现
需要额外处理媒体文件、关系映射两个部分,修改后的代码如下(兼容PHP7及以上版本):
function merge2Docx($document1, $document2, $outputfile){ if (!file_exists($document1) || !file_exists($document2)) { return null; } $zip1 = new clsTbsZip(); $zip2 = new clsTbsZip(); // 读取第一个文档的所有内容 $zip1->Open($document1); $content1 = $zip1->FileRead('word/document.xml'); // 读取第一个文档的关系映射 $rels1 = $zip1->FileRead('word/_rels/document.xml.rels'); // 读取第一个文档的所有媒体文件 $media1 = []; foreach ($zip1->GetFileList() as $file) { if (substr($file, 0, 11) === 'word/media/') { $media1[$file] = $zip1->FileRead($file); } } $zip1->Close(); // 提取第一个文档的正文内容 $p = strpos($content1, '<w:body'); if ($p === false) return null; $p = strpos($content1, '>', $p); $content1 = substr($content1, $p + 1); $p = strpos($content1, '</w:body>'); if ($p === false) return null; $content1_body = substr($content1, 0, $p); // 处理第二个文档 $zip2->Open($document2); $content2 = $zip2->FileRead('word/document.xml'); $rels2 = $zip2->FileRead('word/_rels/document.xml.rels'); // 获取第二个文档现有媒体文件的最大序号,避免重名 $max_media_idx = 0; foreach ($zip2->GetFileList() as $file) { if (substr($file, 0, 16) === 'word/media/image') { preg_match('/image(\d+)\./', $file, $matches); if (isset($matches[1]) && intval($matches[1]) > $max_media_idx) { $max_media_idx = intval($matches[1]); } } } // 获取第二个文档现有rId的最大序号,避免冲突 $max_rid = 0; preg_match_all('/Id="rId(\d+)"/', $rels2, $rid_matches); if (!empty($rid_matches[1])) { $max_rid = max(array_map('intval', $rid_matches[1])); } // 处理第一个文档的关系映射和媒体文件 $rid_map = []; preg_match_all('/<Relationship Id="rId(\d+)" Type=".*image" Target="media\/([^"]+)"\/?>/', $rels1, $rel_matches, PREG_SET_ORDER); foreach ($rel_matches as $rel) { $old_rid = $rel[1]; $old_media_name = $rel[2]; $new_rid = ++$max_rid; $new_media_idx = ++$max_media_idx; $new_media_name = preg_replace('/image\d+\./', "image{$new_media_idx}.", $old_media_name); // 记录rid映射 $rid_map["rId{$old_rid}"] = "rId{$new_rid}"; // 追加新的关系到第二个文档的rels $new_rel = str_replace(["Id=\"rId{$old_rid}\"", "Target=\"media/{$old_media_name}\""], ["Id=\"rId{$new_rid}\"", "Target=\"media/{$new_media_name}\""], $rel[0]); $rels2 = str_replace('</Relationships>', $new_rel . '</Relationships>', $rels2); // 重命名媒体文件并写入第二个文档 $old_media_path = "word/media/{$old_media_name}"; if (isset($media1[$old_media_path])) { $zip2->FileAdd("word/media/{$new_media_name}", $media1[$old_media_path], TBSZIP_STRING); } } // 替换第一个文档正文里的所有旧rId为新的 foreach ($rid_map as $old_rid => $new_rid) { $content1_body = str_replace($old_rid, $new_rid, $content1_body); } // 拼接正文内容,加分页符 $content1_body = '<w:p><w:r><w:br w:type="page" /><w:lastRenderedPageBreak/></w:r></w:p>' . $content1_body; // 合并到第二个文档正文 $p = strpos($content2, '</w:body>'); if ($p === false) return null; $content2 = substr($content2, 0, $p) . $content1_body . substr($content2, $p); $zip2->FileReplace('word/document.xml', $content2, TBSZIP_STRING); // 更新第二个文档的rels文件 $zip2->FileReplace('word/_rels/document.xml.rels', $rels2, TBSZIP_STRING); // 输出文件 $zip2->Flush(TBSZIP_FILE, $outputfile); $zip2->Close(); return file_exists($outputfile) ? $outputfile : null; }
注意事项
- 上述代码仅处理了图片类型的资源,如果你的docx里还有嵌入的OLE对象、图表等其他类型资源,需要对应扩展关系类型的匹配规则
- 如果需要合并两个以上的docx,循环执行上述逻辑即可
- 操作前建议备份原文件,避免数据丢失
内容的提问来源于stack exchange,提问作者An Pham Karion Co.Ltd
相关产品推荐
相关产品推荐

