导入
This commit is contained in:
@@ -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)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user