|
|
@ -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; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |