作者堆叠 专刊

This commit is contained in:
wyn
2026-07-29 17:05:42 +08:00
parent f583f67e24
commit 1ee795b6b9
10 changed files with 2833 additions and 141 deletions

View File

@@ -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/oMathParaDOM 不可用时兜底)
*/
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 目录路径