This commit is contained in:
2025-12-16 15:37:34 +08:00
parent 5030c4c277
commit 29d1999c2d
+247 -43
View File
@@ -256,7 +256,10 @@ class SkuController extends BaseController
}
/**
* docx 转 HTML(富文本可用,图片上传 OSS)
* docx 转 HTML(自动上传图片到 OSS)
*
* @param string $docxPath docx 文件绝对路径
* @return string HTML
*/
function docxToHtmlWithImages(string $docxPath): string
{
@@ -264,6 +267,247 @@ class SkuController extends BaseController
return '';
}
try {
// 1. 创建临时目录
$tmpDir = runtime_path('tmp/' . uniqid('docx_', true));
if (!is_dir($tmpDir)) {
mkdir($tmpDir, 0755, true);
}
// 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);
}
return $html;
}
/**
* 将 XML 节点转换为 HTML
*/
private function convertNodeToHtml(DOMNode $node, DOMXPath $xpath): string
{
$html = '';
$nodeName = $node->nodeName;
switch ($nodeName) {
case 'w:p': // 段落
$html .= '<p>';
$textNodes = $xpath->query('.//w:t', $node);
foreach ($textNodes as $textNode) {
$html .= htmlspecialchars($textNode->nodeValue);
}
$html .= '</p>';
break;
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;
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;
$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. 确保图片有正确的样式
$html = preg_replace_callback(
'/<img([^>]+)>/',
function($matches) {
$attrs = $matches[1];
// 检查是否有样式
if (!str_contains($attrs, 'style=')) {
return '<img' . $attrs . ' style="max-width: 100%; height: auto;">';
}
return '<img' . $attrs . '>';
},
$html
);
return trim($html);
}
/**
* 删除目录
*/
private function deleteDirectory(string $dir): bool
{
if (!is_dir($dir)) {
return false;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
is_dir($path) ? $this->deleteDirectory($path) : @unlink($path);
}
return @rmdir($dir);
}
/**
* docx 转 HTML(富文本可用,图片上传 OSS)
*/
/*function docxToHtmlWithImages(string $docxPath): string
{
if (!is_file($docxPath)) {
return '';
}
$phpWord = \PhpOffice\PhpWord\IOFactory::load($docxPath);
$tmpDir = runtime_path('tmp/' . uniqid());
@@ -301,12 +545,10 @@ class SkuController extends BaseController
);
// // 2️⃣ 关键:HTML 清洗(适配 wangEditor)
// $html = $this->cleanWordHtmlForWangEditor($html);
// 4. 清洗 HTML(关键)
$html = $this->cleanDocHtml($html);
$html = $this->cleanWordHtmlForWangEditor($html);
return $html;
}
}*/
function deleteDir(string $dir): void
{
@@ -319,44 +561,6 @@ class SkuController extends BaseController
@rmdir($dir);
}
/**
* 清洗 doc/docx 转出的 HTML,适配富文本编辑器
*/
function cleanDocHtml(string $html): string
{
libxml_use_internal_errors(true);
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->loadHTML(
'<?xml encoding="UTF-8">' . $html,
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
$xpath = new DOMXPath($dom);
// 1. 删除 head / meta / style / script
foreach (['head', 'meta', 'style', 'script', 'xml'] as $tag) {
foreach ($xpath->query("//{$tag}") as $node) {
$node->parentNode?->removeChild($node);
}
}
// 2. 只取 body 内容
$body = $dom->getElementsByTagName('body')->item(0);
if ($body) {
$innerHtml = '';
foreach ($body->childNodes as $child) {
$innerHtml .= $dom->saveHTML($child);
}
} else {
$innerHtml = $dom->saveHTML();
}
libxml_clear_errors();
return trim($innerHtml);
}
function cleanWordHtmlForWangEditor(string $html): string
{
// 1. 移除 Word 专属标签