This commit is contained in:
2025-12-16 15:42:51 +08:00
parent 29d1999c2d
commit 65df91fa68
+63 -212
View File
@@ -256,10 +256,7 @@ class SkuController extends BaseController
}
/**
* docx 转 HTML(自动上传图片到 OSS)
*
* @param string $docxPath docx 文件绝对路径
* @return string HTML
* docx 转 HTML(富文本可用,图片上传 OSS)
*/
function docxToHtmlWithImages(string $docxPath): string
{
@@ -267,238 +264,92 @@ class SkuController extends BaseController
return '';
}
try {
// 1. 创建临时目录
$tmpDir = runtime_path('tmp/' . uniqid('docx_', true));
if (!is_dir($tmpDir)) {
mkdir($tmpDir, 0755, true);
}
// 1. 加载 docx
$phpWord = WordIOFactory::load($docxPath, 'Word2007');
// 2. 解压 docx 文件(docx 本质上是 zip 文件)
$zip = new ZipArchive();
if ($zip->open($docxPath) === true) {
$zip->extractTo($tmpDir);
$zip->close();
} else {
throw new Exception('无法解压 docx 文件');
}
// 3. 读取文档内容
$documentXml = $tmpDir . '/word/document.xml';
if (!is_file($documentXml)) {
throw new Exception('文档内容文件不存在');
}
// 4. 创建 DOM 对象
$dom = new DOMDocument();
$dom->load($documentXml);
// 5. 处理图片
$uploadService = new UploadService();
$images = $dom->getElementsByTagName('drawing');
foreach ($images as $image) {
// 查找图片引用
$blip = $image->getElementsByTagName('blip');
if ($blip->length > 0) {
$embedId = $blip->item(0)->getAttribute('r:embed');
if ($embedId) {
// 查找关系文件中的图片路径
$relationshipsXml = $tmpDir . '/word/_rels/document.xml.rels';
if (is_file($relationshipsXml)) {
$relsDom = new DOMDocument();
$relsDom->load($relationshipsXml);
$xpath = new DOMXPath($relsDom);
$xpath->registerNamespace('r', 'http://schemas.openxmlformats.org/package/2006/relationships');
$query = "//r:Relationship[@Id='$embedId']";
$nodes = $xpath->query($query);
if ($nodes->length > 0) {
$target = $nodes->item(0)->getAttribute('Target');
$imagePath = $tmpDir . '/word/' . $target;
if (is_file($imagePath)) {
// 上传到 OSS
$ossUrl = $uploadService->uploadLocalFile([
'path' => $imagePath,
'scene' => 'desc'
]);
// 创建 img 标签
$imgNode = $dom->createElement('img');
$imgNode->setAttribute('src', $ossUrl);
$imgNode->setAttribute('alt', '图片');
// 替换 drawing 标签
$image->parentNode->replaceChild($imgNode, $image);
}
}
}
}
}
}
// 6. 将文档内容转换为 HTML
$htmlContent = $this->convertDocumentXmlToHtml($dom, $tmpDir);
// 7. 清理 HTML
$htmlContent = $this->cleanHtmlContent($htmlContent);
// 8. 清理临时目录
$this->deleteDirectory($tmpDir);
return $htmlContent;
} catch (\Exception $e) {
// 清理临时目录
if (isset($tmpDir) && is_dir($tmpDir)) {
$this->deleteDirectory($tmpDir);
}
error_log("docx转HTML失败: " . $e->getMessage());
return '';
}
}
/**
* 将 document.xml 转换为 HTML
*/
private function convertDocumentXmlToHtml(DOMDocument $dom, string $tmpDir): string
{
$xpath = new DOMXPath($dom);
$xpath->registerNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
$html = '';
$bodyNodes = $xpath->query('//w:body/w:*');
foreach ($bodyNodes as $node) {
$html .= $this->convertNodeToHtml($node, $xpath);
// 2. 临时目录(非常重要)
$tmpDir = runtime_path('tmp/' . uniqid());
if (!is_dir($tmpDir)) {
mkdir($tmpDir, 0755, true);
}
return $html;
}
$tmpHtml = $tmpDir . '/index.html';
/**
* 将 XML 节点转换为 HTML
*/
private function convertNodeToHtml(DOMNode $node, DOMXPath $xpath): string
{
$html = '';
$nodeName = $node->nodeName;
// 3. 导出 HTML
$writer = WordIOFactory::createWriter($phpWord, 'HTML');
$writer->save($tmpHtml);
switch ($nodeName) {
case 'w:p': // 段落
$html .= '<p>';
$textNodes = $xpath->query('.//w:t', $node);
foreach ($textNodes as $textNode) {
$html .= htmlspecialchars($textNode->nodeValue);
}
$html .= '</p>';
break;
$html = file_get_contents($tmpHtml);
case 'w:tbl': // 表格
$html .= '<table border="1" style="border-collapse: collapse; width: 100%;">';
$rows = $xpath->query('.//w:tr', $node);
foreach ($rows as $row) {
$html .= '<tr>';
$cells = $xpath->query('.//w:tc', $row);
foreach ($cells as $cell) {
$html .= '<td>';
$cellTextNodes = $xpath->query('.//w:t', $cell);
foreach ($cellTextNodes as $cellText) {
$html .= htmlspecialchars($cellText->nodeValue);
}
$html .= '</td>';
}
$html .= '</tr>';
}
$html .= '</table>';
break;
// 4. 清洗 Word 垃圾 HTML(关键)
$html = $this->cleanWordHtml($html);
case 'w:r': // 文本运行(可能是加粗等格式)
$bold = $xpath->query('.//w:b', $node)->length > 0;
$italic = $xpath->query('.//w:i', $node)->length > 0;
$underline = $xpath->query('.//w:u', $node)->length > 0;
$uploadService = new UploadService();
$text = '';
$textNodes = $xpath->query('.//w:t', $node);
foreach ($textNodes as $textNode) {
$text .= htmlspecialchars($textNode->nodeValue);
}
if ($bold) {
$text = '<strong>' . $text . '</strong>';
}
if ($italic) {
$text = '<em>' . $text . '</em>';
}
if ($underline) {
$text = '<u>' . $text . '</u>';
}
$html .= $text;
break;
default:
// 处理其他节点
break;
}
return $html;
}
/**
* 清理 HTML 内容
*/
private function cleanHtmlContent(string $html): string
{
// 1. 清理多余的空格和换行
$html = preg_replace('/\s+/', ' ', $html);
$html = preg_replace('/>\s+</', '><', $html);
// 2. 清理空的段落
$html = preg_replace('/<p>\s*<\/p>/', '', $html);
// 3. 确保图片有正确的样式
// 5. 处理图片
$html = preg_replace_callback(
'/<img([^>]+)>/',
function($matches) {
$attrs = $matches[1];
'/<img[^>]+src="([^"]+)"[^>]*>/i',
function ($matches) use ($tmpDir, $uploadService) {
$src = $matches[1];
// 检查是否有样式
if (!str_contains($attrs, 'style=')) {
return '<img' . $attrs . ' style="max-width: 100%; height: auto;">';
// 只处理本地图片
if (preg_match('#^(http|https|data:)#i', $src)) {
return $matches[0];
}
return '<img' . $attrs . '>';
$localImg = realpath($tmpDir . '/' . $src);
if (!$localImg || !is_file($localImg)) {
return $matches[0];
}
// 上传 OSS
$ossUrl = $uploadService->uploadLocalFile([
'path' => $localImg,
'scene' => 'desc'
]);
return str_replace($src, $ossUrl, $matches[0]);
},
$html
);
// 6. 清理临时目录
$this->deleteDir($tmpDir);
return trim($html);
}
/**
* 删除目录
*/
private function deleteDirectory(string $dir): bool
function cleanWordHtml(string $html): string
{
if (!is_dir($dir)) {
return false;
// 删除 Word 注释
$html = preg_replace('/<!--(.|\s)*?-->/', '', $html);
// 删除 style / meta / link
$html = preg_replace('/<(style|meta|link)[^>]*>.*?<\/\1>/is', '', $html);
// 删除 xml 标签
$html = preg_replace('/<xml>.*?<\/xml>/is', '', $html);
// 删除 mso-xxx 样式
$html = preg_replace('/\s*mso-[^:]+:[^;"]+;?/i', '', $html);
// 清理 class="Mso..."
$html = preg_replace('/class="Mso[^"]*"/i', '', $html);
// 删除空 span
$html = preg_replace('/<span[^>]*>\s*<\/span>/i', '', $html);
// 删除 Office 条件注释
$html = preg_replace('/<!\[if.*?\]>.*?<!\[endif\]>/is', '', $html);
// 只保留 body 内容
if (preg_match('/<body[^>]*>(.*?)<\/body>/is', $html, $m)) {
$html = $m[1];
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
is_dir($path) ? $this->deleteDirectory($path) : @unlink($path);
}
return @rmdir($dir);
return $html;
}
/**
* docx 转 HTML(富文本可用,图片上传 OSS)
*/