Browse Source

修改 bug

master
王泽彦 7 months ago
parent
commit
05e2c05622
  1. 677
      niucloud/app/service/admin/document/DocumentTemplateService.php

677
niucloud/app/service/admin/document/DocumentTemplateService.php

@ -1791,6 +1791,9 @@ class DocumentTemplateService extends BaseAdminService
'file_size' => filesize($outputPath) 'file_size' => filesize($outputPath)
]); ]);
// 处理文档中的URL图片(第二次处理)
$this->processUrlImagesInDocument($outputPath, $fillValues);
return true; return true;
} finally { } finally {
@ -1818,18 +1821,24 @@ class DocumentTemplateService extends BaseAdminService
private function replaceXmlPlaceholders(string $xmlContent, array $fillValues): string private function replaceXmlPlaceholders(string $xmlContent, array $fillValues): string
{ {
$modifiedContent = $xmlContent; $modifiedContent = $xmlContent;
// 首先清理被格式化标签分割的占位符
$modifiedContent = $this->cleanBrokenPlaceholdersInXml($modifiedContent);
$replacementCount = 0; $replacementCount = 0;
foreach ($fillValues as $placeholder => $value) { foreach ($fillValues as $placeholder => $value) {
// 确保占位符格式正确 // 使用正则表达式匹配占位符,支持空格、换行符、制表符
$searchPattern = '{{' . $placeholder . '}}'; // 模式:{{ + 任意空白字符 + placeholder + 任意空白字符 + }}
$escapedPlaceholder = preg_quote($placeholder, '/');
$searchPattern = '/\{\{\s*' . $escapedPlaceholder . '\s*\}\}/';
// 转义特殊字符,确保安全的XML替换 // 转义特殊字符,确保安全的XML替换
$safeValue = htmlspecialchars((string)$value, ENT_XML1, 'UTF-8'); $safeValue = htmlspecialchars((string)$value, ENT_XML1, 'UTF-8');
// 进行替换 // 使用正则替换
$beforeLength = strlen($modifiedContent); $beforeLength = strlen($modifiedContent);
$modifiedContent = str_replace($searchPattern, $safeValue, $modifiedContent, $count); $modifiedContent = preg_replace($searchPattern, $safeValue, $modifiedContent, -1, $count);
$afterLength = strlen($modifiedContent); $afterLength = strlen($modifiedContent);
if ($count > 0) { if ($count > 0) {
@ -1857,6 +1866,234 @@ class DocumentTemplateService extends BaseAdminService
return $modifiedContent; return $modifiedContent;
} }
/**
* 清理XML中被格式化标签分割的占位符
* @param string $xmlContent
* @return string
*/
private function cleanBrokenPlaceholdersInXml(string $xmlContent): string
{
$cleanedContent = $xmlContent;
Log::info('开始清理分割的占位符');
// 多阶段清理策略:从简单到复杂
// 第一阶段:处理完整的双大括号分割情况
$cleanedContent = $this->fixCompleteBracketSplits($cleanedContent);
// 第二阶段:处理单个大括号分割情况
$cleanedContent = $this->fixSingleBracketSplits($cleanedContent);
// 第三阶段:通用清理,移除占位符内部的XML标签
$cleanedContent = $this->generalPlaceholderCleanup($cleanedContent);
// 第四阶段:最终验证和修复
$cleanedContent = $this->finalPlaceholderValidation($cleanedContent);
Log::info('占位符清理完成');
return $cleanedContent;
}
/**
* 修复完整双大括号被分割的情况
* @param string $content
* @return string
*/
private function fixCompleteBracketSplits(string $content): string
{
Log::info('阶段1:修复完整双大括号分割');
$patterns = [
// 模式1: {{</w:t>...<w:t>内容}}
'/\{\{<\/w:t>.*?<w:t[^>]*>([^<]*)\}\}/',
// 模式2: {{</w:t></w:r><w:r>...<w:t>内容}}
'/\{\{<\/w:t><\/w:r><w:r[^>]*>.*?<w:t[^>]*>([^<]*)\}\}/',
// 模式3: 包含rPr格式标签的复杂嵌套
'/\{\{<\/w:t><\/w:r><w:r[^>]*><w:rPr>.*?<\/w:rPr><w:t[^>]*>([^<]*)\}\}/'
];
foreach ($patterns as $index => $pattern) {
$content = preg_replace_callback($pattern, function($matches) use ($index) {
$placeholderContent = $matches[1];
$result = '{{' . $placeholderContent . '}}';
Log::info('修复完整双大括号分割', [
'pattern_index' => $index + 1,
'original' => substr($matches[0], 0, 100) . '...',
'fixed' => $result,
'placeholder_content' => $placeholderContent
]);
return $result;
}, $content);
}
return $content;
}
/**
* 修复单个大括号被分割的情况
* @param string $content
* @return string
*/
private function fixSingleBracketSplits(string $content): string
{
Log::info('阶段2:修复单个大括号分割');
// 处理左大括号被分割:{</w:t>...<w:t>{内容}}
$leftBracketPatterns = [
// 标准模式:{</w:t></w:r><w:r>...<w:t>{内容}}
'/\{<\/w:t><\/w:r><w:r[^>]*>.*?<w:t[^>]*>\{([^}]*)\}\}/',
// 包含rPr的复杂模式
'/\{<\/w:t><\/w:r><w:r[^>]*><w:rPr>.*?<\/w:rPr><w:t[^>]*>\{([^}]*)\}\}/',
// 简单模式:{</w:t>...<w:t>{内容}}
'/\{<\/w:t>.*?<w:t[^>]*>\{([^}]*)\}\}/'
];
foreach ($leftBracketPatterns as $index => $pattern) {
$content = preg_replace_callback($pattern, function($matches) use ($index) {
$placeholderContent = $matches[1];
$result = '{{' . $placeholderContent . '}}';
Log::info('修复左大括号分割', [
'pattern_index' => $index + 1,
'original' => substr($matches[0], 0, 100) . '...',
'fixed' => $result,
'placeholder_content' => $placeholderContent
]);
return $result;
}, $content);
}
// 处理右大括号被分割:{{内容}</w:t>...<w:t>}
$rightBracketPatterns = [
// 标准模式:{{内容}</w:t></w:r><w:r>...<w:t>}
'/\{\{([^}]*)\}<\/w:t><\/w:r><w:r[^>]*>.*?<w:t[^>]*>\}/',
// 包含rPr的复杂模式
'/\{\{([^}]*)\}<\/w:t><\/w:r><w:r[^>]*><w:rPr>.*?<\/w:rPr><w:t[^>]*>\}/',
// 简单模式:{{内容}</w:t>...<w:t>}
'/\{\{([^}]*)\}<\/w:t>.*?<w:t[^>]*>\}/'
];
foreach ($rightBracketPatterns as $index => $pattern) {
$content = preg_replace_callback($pattern, function($matches) use ($index) {
$placeholderContent = $matches[1];
$result = '{{' . $placeholderContent . '}}';
Log::info('修复右大括号分割', [
'pattern_index' => $index + 1,
'original' => substr($matches[0], 0, 100) . '...',
'fixed' => $result,
'placeholder_content' => $placeholderContent
]);
return $result;
}, $content);
}
return $content;
}
/**
* 通用占位符清理:移除占位符内部的XML标签
* @param string $content
* @return string
*/
private function generalPlaceholderCleanup(string $content): string
{
Log::info('阶段3:通用占位符清理');
// 匹配并清理占位符内部的XML标签
$generalPattern = '/\{\{([^}]*?)<[^>]*?>([^}]*?)\}\}/';
$iterations = 0;
$maxIterations = 10; // 防止无限循环
do {
$beforeLength = strlen($content);
$iterations++;
$content = preg_replace_callback($generalPattern, function($matches) {
// 移除XML标签,只保留纯文本
$content = $matches[1] . $matches[2];
$cleanContent = preg_replace('/<[^>]*?>/', '', $content);
$result = '{{' . $cleanContent . '}}';
Log::info('通用占位符清理', [
'original' => substr($matches[0], 0, 50) . '...',
'cleaned' => $result,
'content' => $cleanContent
]);
return $result;
}, $content);
$afterLength = strlen($content);
Log::info('通用清理迭代', [
'iteration' => $iterations,
'length_change' => $afterLength - $beforeLength,
'has_more_matches' => preg_match($generalPattern, $content)
]);
} while ($beforeLength !== $afterLength &&
preg_match($generalPattern, $content) &&
$iterations < $maxIterations);
return $content;
}
/**
* 最终占位符验证和修复
* @param string $content
* @return string
*/
private function finalPlaceholderValidation(string $content): string
{
Log::info('阶段4:最终验证和修复');
// 查找所有可能的占位符模式
if (preg_match_all('/\{[^{}]*\}/', $content, $matches)) {
foreach ($matches[0] as $match) {
// 检查是否为不完整的占位符(只有一个大括号)
if (preg_match('/^\{[^{}]+\}$/', $match)) {
// 查看前后文是否有对应的大括号
$singleBracketPattern = '/' . preg_quote($match, '/') . '/';
// 尝试修复为双大括号
$possibleFix = '{' . $match . '}';
Log::info('发现可能的不完整占位符', [
'found' => $match,
'possible_fix' => $possibleFix
]);
}
}
}
// 查找并统计最终的占位符数量
$finalPlaceholders = [];
if (preg_match_all('/\{\{([^}]+)\}\}/', $content, $matches)) {
$finalPlaceholders = $matches[1];
}
Log::info('最终占位符统计', [
'count' => count($finalPlaceholders),
'placeholders' => $finalPlaceholders
]);
return $content;
}
/** /**
* 递归添加目录到ZIP文件 * 递归添加目录到ZIP文件
* @param \ZipArchive $zip ZIP文件对象 * @param \ZipArchive $zip ZIP文件对象
@ -2134,4 +2371,436 @@ class DocumentTemplateService extends BaseAdminService
return null; return null;
} }
/**
* 处理Word文档中的URL图片,将URL转换为嵌入的图片附件
* @param string $documentPath Word文档路径
* @param array $fillValues 填充值数组(用于识别哪些是图片URL)
* @return bool
*/
private function processUrlImagesInDocument(string $documentPath, array $fillValues): bool
{
try {
Log::info('开始处理Word文档中的URL图片', [
'document_path' => $documentPath,
'fill_values_count' => count($fillValues)
]);
// 创建临时工作目录
$tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'word_url_processing_' . uniqid();
if (!mkdir($tempDir, 0755, true)) {
throw new \Exception('无法创建临时工作目录');
}
try {
// 1. 解压Word文档到临时目录
$zip = new \ZipArchive();
$result = $zip->open($documentPath);
if ($result !== TRUE) {
throw new \Exception('无法打开Word文档,错误代码:' . $result);
}
if (!$zip->extractTo($tempDir)) {
$zip->close();
throw new \Exception('无法解压Word文档');
}
$zip->close();
// 2. 读取document.xml文件
$documentXmlPath = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . 'document.xml';
if (!file_exists($documentXmlPath)) {
throw new \Exception('Word文档缺少document.xml文件');
}
$xmlContent = file_get_contents($documentXmlPath);
if ($xmlContent === false) {
throw new \Exception('无法读取document.xml文件内容');
}
// 3. 查找并处理URL图片
$urlImageMap = $this->findUrlImagesInFillValues($fillValues);
if (empty($urlImageMap)) {
Log::info('未找到需要处理的URL图片');
return true;
}
// 4. 下载图片并创建关系文件
$imageRelations = $this->downloadAndCreateImageRelations($urlImageMap, $tempDir);
if (empty($imageRelations)) {
Log::warning('没有成功下载任何图片');
return true;
}
// 5. 更新document.xml中的URL为图片引用
$modifiedXmlContent = $this->replaceUrlsWithImageReferences($xmlContent, $imageRelations);
// 6. 更新关系文件
$this->updateDocumentRelations($tempDir, $imageRelations);
// 7. 写回修改后的document.xml
if (file_put_contents($documentXmlPath, $modifiedXmlContent) === false) {
throw new \Exception('无法写入修改后的document.xml');
}
// 8. 重新打包Word文档
$newZip = new \ZipArchive();
$result = $newZip->open($documentPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
if ($result !== TRUE) {
throw new \Exception('无法重新打开Word文件,错误代码:' . $result);
}
// 递归添加所有文件到ZIP
$this->addDirectoryToZip($newZip, $tempDir, '');
if (!$newZip->close()) {
throw new \Exception('无法保存修改后的Word文件');
}
Log::info('URL图片处理完成', [
'processed_images' => count($imageRelations),
'document_size' => filesize($documentPath)
]);
return true;
} finally {
// 清理临时目录
$this->removeDirectory($tempDir);
}
} catch (\Exception $e) {
Log::error('处理Word文档URL图片失败', [
'document_path' => $documentPath,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// 即使失败也不影响主流程
return false;
}
}
/**
* 从填充值中查找URL图片
* @param array $fillValues
* @return array
*/
private function findUrlImagesInFillValues(array $fillValues): array
{
$urlImages = [];
foreach ($fillValues as $placeholder => $value) {
if (is_string($value)) {
// 检查是否为HTTP/HTTPS URL
if (preg_match('/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i', $value)) {
$urlImages[$placeholder] = $value;
Log::info('发现URL图片', [
'placeholder' => $placeholder,
'url' => $value
]);
}
}
}
return $urlImages;
}
/**
* 下载图片并创建图片关系
* @param array $urlImageMap
* @param string $tempDir
* @return array
*/
private function downloadAndCreateImageRelations(array $urlImageMap, string $tempDir): array
{
$imageRelations = [];
$relationId = 1000; // 起始关系ID,避免与现有关系冲突
// 确保media目录存在
$mediaDir = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . 'media';
if (!is_dir($mediaDir)) {
mkdir($mediaDir, 0755, true);
}
foreach ($urlImageMap as $placeholder => $url) {
try {
// 下载图片
$imageData = $this->downloadImageData($url);
if (!$imageData) {
continue;
}
// 确定文件扩展名
$pathInfo = pathinfo(parse_url($url, PHP_URL_PATH));
$extension = strtolower($pathInfo['extension'] ?? 'jpg');
// 验证图片数据
$tempImagePath = tempnam(sys_get_temp_dir(), 'img_validate_');
file_put_contents($tempImagePath, $imageData);
$imageInfo = getimagesize($tempImagePath);
unlink($tempImagePath);
if ($imageInfo === false) {
Log::warning('下载的文件不是有效图片', ['url' => $url]);
continue;
}
// 生成文件名和关系ID
$fileName = 'image' . $relationId . '.' . $extension;
$filePath = $mediaDir . DIRECTORY_SEPARATOR . $fileName;
$relationIdStr = 'rId' . $relationId;
// 保存图片文件
if (file_put_contents($filePath, $imageData) === false) {
Log::error('保存图片文件失败', ['file_path' => $filePath]);
continue;
}
// 记录图片关系信息
$imageRelations[$placeholder] = [
'url' => $url,
'relation_id' => $relationIdStr,
'file_name' => $fileName,
'file_path' => $filePath,
'width' => $imageInfo[0],
'height' => $imageInfo[1],
'mime_type' => $imageInfo['mime']
];
$relationId++;
Log::info('图片下载成功', [
'placeholder' => $placeholder,
'url' => $url,
'file_name' => $fileName,
'size' => $imageInfo[0] . 'x' . $imageInfo[1]
]);
} catch (\Exception $e) {
Log::error('处理图片失败', [
'placeholder' => $placeholder,
'url' => $url,
'error' => $e->getMessage()
]);
}
}
return $imageRelations;
}
/**
* 下载图片数据
* @param string $url
* @return string|false
*/
private function downloadImageData(string $url)
{
try {
$context = stream_context_create([
'http' => [
'timeout' => 30,
'user_agent' => 'Mozilla/5.0 (compatible; Document Generator)',
'follow_location' => true,
'max_redirects' => 3
]
]);
return file_get_contents($url, false, $context);
} catch (\Exception $e) {
Log::error('下载图片数据失败:' . $e->getMessage(), ['url' => $url]);
return false;
}
}
/**
* 将XML中的URL替换为图片引用
* @param string $xmlContent
* @param array $imageRelations
* @return string
*/
private function replaceUrlsWithImageReferences(string $xmlContent, array $imageRelations): string
{
$modifiedContent = $xmlContent;
foreach ($imageRelations as $placeholder => $relation) {
$url = $relation['url'];
// 构建图片引用XML
$imageXml = $this->generateImageXml($relation);
// 替换URL为图片引用
$modifiedContent = str_replace($url, $imageXml, $modifiedContent);
Log::info('URL替换为图片引用', [
'placeholder' => $placeholder,
'url' => $url,
'relation_id' => $relation['relation_id']
]);
}
return $modifiedContent;
}
/**
* 生成图片引用XML
* @param array $relation
* @return string
*/
private function generateImageXml(array $relation): string
{
// 强制限制最大尺寸
$maxWidth = 40; // 最大宽度(像素)
$maxHeight = 30; // 最大高度(像素)
$originalWidth = $relation['width'];
$originalHeight = $relation['height'];
Log::info('图片原始尺寸', [
'original_width' => $originalWidth,
'original_height' => $originalHeight,
'max_width' => $maxWidth,
'max_height' => $maxHeight
]);
// 计算缩放比例 - 如果图片超过最大尺寸就缩放,否则使用较小的固定尺寸
$displayWidth = $maxWidth;
$displayHeight = $maxHeight;
if ($originalWidth > 0 && $originalHeight > 0) {
// 计算保持宽高比的缩放
$widthRatio = $maxWidth / $originalWidth;
$heightRatio = $maxHeight / $originalHeight;
$ratio = min($widthRatio, $heightRatio); // 移除1的限制,允许缩放
$displayWidth = (int)($originalWidth * $ratio);
$displayHeight = (int)($originalHeight * $ratio);
// 确保不超过最大尺寸
$displayWidth = min($displayWidth, $maxWidth);
$displayHeight = min($displayHeight, $maxHeight);
}
Log::info('图片显示尺寸', [
'display_width' => $displayWidth,
'display_height' => $displayHeight,
'ratio' => isset($ratio) ? $ratio : 'fixed'
]);
// Word中的尺寸单位转换(像素转EMU)
$emuWidth = $displayWidth * 9525; // 1像素 = 9525 EMU
$emuHeight = $displayHeight * 9525;
$relationId = $relation['relation_id'];
return '<w:r>
<w:drawing>
<wp:inline distT="0" distB="0" distL="0" distR="0">
<wp:extent cx="' . $emuWidth . '" cy="' . $emuHeight . '"/>
<wp:effectExtent l="0" t="0" r="0" b="0"/>
<wp:docPr id="1" name="图片"/>
<wp:cNvGraphicFramePr>
<a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
</wp:cNvGraphicFramePr>
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:nvPicPr>
<pic:cNvPr id="1" name="图片"/>
<pic:cNvPicPr/>
</pic:nvPicPr>
<pic:blipFill>
<a:blip r:embed="' . $relationId . '"/>
<a:stretch>
<a:fillRect/>
</a:stretch>
</pic:blipFill>
<pic:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="' . $emuWidth . '" cy="' . $emuHeight . '"/>
</a:xfrm>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
</pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
</w:r>';
}
/**
* 更新文档关系文件
* @param string $tempDir
* @param array $imageRelations
* @return void
*/
private function updateDocumentRelations(string $tempDir, array $imageRelations): void
{
try {
$relsPath = $tempDir . DIRECTORY_SEPARATOR . 'word' . DIRECTORY_SEPARATOR . '_rels' . DIRECTORY_SEPARATOR . 'document.xml.rels';
// 如果关系文件不存在,创建基础关系文件
if (!file_exists($relsPath)) {
$relsDir = dirname($relsPath);
if (!is_dir($relsDir)) {
mkdir($relsDir, 0755, true);
}
$baseRelsContent = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>';
file_put_contents($relsPath, $baseRelsContent);
}
// 读取现有关系文件
$relsContent = file_get_contents($relsPath);
if ($relsContent === false) {
throw new \Exception('无法读取关系文件');
}
// 解析XML
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
if (!$dom->loadXML($relsContent)) {
throw new \Exception('无法解析关系文件XML');
}
$relationshipsElement = $dom->documentElement;
// 添加图片关系
foreach ($imageRelations as $relation) {
$relationshipElement = $dom->createElement('Relationship');
$relationshipElement->setAttribute('Id', $relation['relation_id']);
$relationshipElement->setAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image');
$relationshipElement->setAttribute('Target', 'media/' . $relation['file_name']);
$relationshipsElement->appendChild($relationshipElement);
}
// 写回关系文件
if (file_put_contents($relsPath, $dom->saveXML()) === false) {
throw new \Exception('无法保存关系文件');
}
Log::info('文档关系文件更新成功', [
'relations_added' => count($imageRelations),
'rels_path' => $relsPath
]);
} catch (\Exception $e) {
Log::error('更新文档关系文件失败', [
'error' => $e->getMessage(),
'temp_dir' => $tempDir
]);
throw $e;
}
}
} }
Loading…
Cancel
Save