作者堆叠 专刊
This commit is contained in:
@@ -20,20 +20,28 @@ class ArticleParserService
|
||||
if (!file_exists($filePath)) {
|
||||
return json_encode(['status' => 5, 'msg' => '"文档不存在:{$filePath}"']);
|
||||
}
|
||||
$processedFilePath = null;
|
||||
try {
|
||||
// 含 OMML 公式时先展平:PhpWord 单独 saveXML(m:oMath) 会丢掉 xmlns:m,触发 loadXML 告警
|
||||
$loadPath = $filePath;
|
||||
if ($this->docxContainsOfficeMath($filePath)) {
|
||||
$processedFilePath = $this->removeEmfFromDocx($filePath);
|
||||
$loadPath = $processedFilePath;
|
||||
}
|
||||
|
||||
// 关键配置:关闭“仅读数据”,保留完整节结构
|
||||
$reader = IOFactory::createReader();
|
||||
$reader->setReadDataOnly(false);
|
||||
Settings::setCompatibility(false);
|
||||
Settings::setOutputEscapingEnabled(true); // 避免XML转义冲突
|
||||
|
||||
$doc = $reader->load($filePath);
|
||||
$sectionCount = count($doc->getSections());
|
||||
// $this->log("✅ 文档直接加载成功,节数量:{$sectionCount}");
|
||||
$this->phpWord = $reader->load($filePath);
|
||||
$this->phpWord = $reader->load($loadPath);
|
||||
$this->sections = $this->phpWord->getSections();
|
||||
} catch (\Throwable $e) {
|
||||
// 预处理:移除 EMF、表格内分页符等 PhpWord 不兼容内容后重试
|
||||
// 预处理:移除 EMF、表格内分页符、OMML 公式等 PhpWord 不兼容内容后重试
|
||||
if ($processedFilePath && is_file($processedFilePath)) {
|
||||
@unlink($processedFilePath);
|
||||
}
|
||||
$processedFilePath = $this->removeEmfFromDocx($filePath);
|
||||
$reader = IOFactory::createReader();
|
||||
$reader->setReadDataOnly(false);
|
||||
@@ -42,8 +50,8 @@ class ArticleParserService
|
||||
|
||||
$this->phpWord = $reader->load($processedFilePath);
|
||||
$this->sections = $this->phpWord->getSections();
|
||||
|
||||
if (is_file($processedFilePath)) {
|
||||
} finally {
|
||||
if ($processedFilePath && is_file($processedFilePath)) {
|
||||
@unlink($processedFilePath);
|
||||
}
|
||||
}
|
||||
@@ -77,7 +85,7 @@ class ArticleParserService
|
||||
}
|
||||
}
|
||||
|
||||
// 3.1 清理表格单元格内分页符(PhpWord 无法解析,会抛 Cannot add PageBreak in Cell)
|
||||
// 3.1 清理表格单元格内分页符、展平 OMML 公式(避免 PhpWord OfficeMathML 缺命名空间告警)
|
||||
$this->sanitizePageBreaksInDocxXmlFiles($tempDir);
|
||||
|
||||
// 4. 重新打包为 DOCX
|
||||
@@ -98,7 +106,24 @@ class ArticleParserService
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 word/*.xml 中表格单元格里的分页符,避免 PhpWord 读取失败
|
||||
* 文档是否含 Word OMML 公式(m:oMath / m:oMathPara)
|
||||
*/
|
||||
private function docxContainsOfficeMath($docxPath): bool
|
||||
{
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($docxPath) !== true) {
|
||||
return false;
|
||||
}
|
||||
$xml = $zip->getFromName('word/document.xml');
|
||||
$zip->close();
|
||||
if ($xml === false || $xml === '') {
|
||||
return false;
|
||||
}
|
||||
return stripos($xml, 'oMath') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理 word/*.xml:表格内分页符 + OMML 公式展平为纯文本
|
||||
*/
|
||||
private function sanitizePageBreaksInDocxXmlFiles($tempDir)
|
||||
{
|
||||
@@ -113,14 +138,14 @@ class ArticleParserService
|
||||
}
|
||||
|
||||
foreach ($xmlFiles as $xmlPath) {
|
||||
$this->sanitizePageBreaksInWordXmlFile($xmlPath);
|
||||
$this->sanitizeWordXmlFile($xmlPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $xmlPath
|
||||
*/
|
||||
private function sanitizePageBreaksInWordXmlFile($xmlPath)
|
||||
private function sanitizeWordXmlFile($xmlPath)
|
||||
{
|
||||
if (!is_file($xmlPath) || !is_readable($xmlPath)) {
|
||||
return;
|
||||
@@ -131,15 +156,72 @@ class ArticleParserService
|
||||
return;
|
||||
}
|
||||
|
||||
// 缺 xmlns:m 时先补到根节点,便于 DOM 解析
|
||||
if (stripos($xml, 'oMath') !== false && stripos($xml, 'xmlns:m=') === false) {
|
||||
$xml = preg_replace(
|
||||
'/<(w:document|w:hdr|w:ftr|w:footnotes|w:endnotes)\b([^>]*)>/',
|
||||
'<$1$2 xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">',
|
||||
$xml,
|
||||
1
|
||||
);
|
||||
if (!is_string($xml)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$prev = libxml_use_internal_errors(true);
|
||||
$dom = new DOMDocument();
|
||||
$dom->preserveWhiteSpace = true;
|
||||
$dom->formatOutput = false;
|
||||
if (@$dom->loadXML($xml) === false) {
|
||||
$ok = $dom->loadXML($xml);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($prev);
|
||||
if ($ok === false) {
|
||||
// DOM 失败时用正则展平公式,避免 PhpWord 再踩 oMath 命名空间问题
|
||||
$flattened = $this->flattenOfficeMathByRegex($xml);
|
||||
if ($flattened !== $xml) {
|
||||
file_put_contents($xmlPath, $flattened);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$xpath = new DOMXPath($dom);
|
||||
$xpath->registerNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
|
||||
$wNs = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
||||
$mNs = 'http://schemas.openxmlformats.org/officeDocument/2006/math';
|
||||
$xpath->registerNamespace('w', $wNs);
|
||||
$xpath->registerNamespace('m', $mNs);
|
||||
|
||||
$changed = false;
|
||||
|
||||
// 先处理 oMathPara,再处理剩余 oMath,保留 m:t 可见文本
|
||||
foreach (['//m:oMathPara', '//m:oMath'] as $query) {
|
||||
$nodes = $xpath->query($query);
|
||||
if (!$nodes || $nodes->length === 0) {
|
||||
continue;
|
||||
}
|
||||
for ($i = $nodes->length - 1; $i >= 0; $i--) {
|
||||
$mathNode = $nodes->item($i);
|
||||
if (!$mathNode || !$mathNode->parentNode) {
|
||||
continue;
|
||||
}
|
||||
$text = '';
|
||||
$tNodes = $xpath->query('.//m:t', $mathNode);
|
||||
if ($tNodes) {
|
||||
foreach ($tNodes as $tNode) {
|
||||
$text .= $tNode->textContent;
|
||||
}
|
||||
}
|
||||
$run = $dom->createElementNS($wNs, 'w:r');
|
||||
$tEl = $dom->createElementNS($wNs, 'w:t');
|
||||
$tEl->appendChild($dom->createTextNode($text));
|
||||
if ($text !== '' && preg_match('/^\s|\s$/u', $text)) {
|
||||
$tEl->setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||
}
|
||||
$run->appendChild($tEl);
|
||||
$mathNode->parentNode->replaceChild($run, $mathNode);
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
$queries = [
|
||||
'//w:tc//w:br[@w:type="page"]',
|
||||
@@ -147,7 +229,6 @@ class ArticleParserService
|
||||
'//w:tc//w:pPr/w:pageBreakBefore',
|
||||
];
|
||||
|
||||
$removed = false;
|
||||
foreach ($queries as $query) {
|
||||
$nodes = $xpath->query($query);
|
||||
if (!$nodes || $nodes->length === 0) {
|
||||
@@ -157,16 +238,55 @@ class ArticleParserService
|
||||
$node = $nodes->item($i);
|
||||
if ($node && $node->parentNode) {
|
||||
$node->parentNode->removeChild($node);
|
||||
$removed = true;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($removed) {
|
||||
if ($changed) {
|
||||
file_put_contents($xmlPath, $dom->saveXML());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 正则展平 OMML:保留 m:t 文本,去掉 oMath/oMathPara(DOM 不可用时兜底)
|
||||
*/
|
||||
private function flattenOfficeMathByRegex(string $xml): string
|
||||
{
|
||||
if (stripos($xml, 'oMath') === false) {
|
||||
return $xml;
|
||||
}
|
||||
|
||||
$toRun = function (string $inner): string {
|
||||
$text = '';
|
||||
if (preg_match_all('/<m:t\b[^>]*>([\s\S]*?)<\/m:t>/i', $inner, $m)) {
|
||||
foreach ($m[1] as $part) {
|
||||
$text .= html_entity_decode(strip_tags($part), ENT_QUOTES | ENT_XML1, 'UTF-8');
|
||||
}
|
||||
}
|
||||
$safe = htmlspecialchars($text, ENT_QUOTES | ENT_XML1, 'UTF-8');
|
||||
$space = ($text !== '' && preg_match('/^\s|\s$/u', $text)) ? ' xml:space="preserve"' : '';
|
||||
return '<w:r><w:t' . $space . '>' . $safe . '</w:t></w:r>';
|
||||
};
|
||||
|
||||
$xml = preg_replace_callback(
|
||||
'/<m:oMathPara\b[^>]*>([\s\S]*?)<\/m:oMathPara>/i',
|
||||
function ($m) use ($toRun) {
|
||||
return $toRun($m[1]);
|
||||
},
|
||||
$xml
|
||||
);
|
||||
$xml = preg_replace_callback(
|
||||
'/<m:oMath\b[^>]*>([\s\S]*?)<\/m:oMath>/i',
|
||||
function ($m) use ($toRun) {
|
||||
return $toRun($m[1]);
|
||||
},
|
||||
is_string($xml) ? $xml : ''
|
||||
);
|
||||
|
||||
return is_string($xml) ? $xml : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归添加目录文件到 ZipArchive
|
||||
* @param string $dir 目录路径
|
||||
|
||||
Reference in New Issue
Block a user