Word 能直接開啟 HTML,但外部資源(圖片、CSS)會丟失。MHTML (MIME HTML) 將所有資源以 Base64 編碼內嵌於單一檔案中,Word 可完美識別並渲染。只需將 .mht 改為 .doc 副檔名即可。
<?php
/**
* MHTML 檔案產生器
* 支援:HTML 內容、圖片內嵌、CSS 內嵌、自訂 MIME 類型
*/
class MhtFileMaker {
private array $headers = [];
private array $parts = [];
private string $boundary;
public function __construct() {
$this->boundary = '--' . strtoupper(md5(microtime() . mt_rand())) . '_MHT_PART';
$this->headers['MIME-Version'] = '1.0';
$this->headers['Content-Type'] = "multipart/related; boundary=\"{$this->boundary}\"; type=\"text/html\"";
$this->headers['X-MimeOLE'] = 'Produced By PHP MhtFileMaker';
}
public function setHeader(string $name, string $value): void {
$this->headers[$name] = $value;
}
public function addPart(string $location, string $mimeType, string $content, string $encoding = 'base64'): void {
if ($encoding === 'base64') {
$content = chunk_split(base64_encode($content), 76);
}
$this->parts[] = compact('location', 'mimeType', 'content', 'encoding');
}
public function getMimeType(string $filename): string {
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
return match ($ext) {
'htm', 'html' => 'text/html',
'txt' => 'text/plain',
'css' => 'text/css',
'jpg', 'jpeg', 'jpe' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
default => 'application/octet-stream',
};
}
public function build(): string {
$out = [];
foreach ($this->headers as $k => $v) {
$out[] = "{$k}: {$v}";
}
$out[] = '';
$out[] = 'This is a multi-part message in MIME format.';
$out[] = '';
foreach ($this->parts as $part) {
$out[] = "--{$this->boundary}";
$out[] = "Content-Type: {$part['mimeType']}";
$out[] = "Content-Transfer-Encoding: {$part['encoding']}";
$out[] = "Content-Location: {$part['location']}";
$out[] = '';
$out[] = $part['content'];
$out[] = '';
}
$out[] = "--{$this->boundary}--";
return implode("\r\n", $out);
}
public function save(string $filename): bool {
$content = $this->build();
return file_put_contents($filename, $content) !== false;
}
}
<?php
/**
* 將 HTML 內容轉換為可含圖片的 Word 文件內容
* @param string $html HTML 來源內容
* @param string $baseUrl 圖片相對路徑補全基礎 URL(需以 / 結尾)
* @param bool $stripLinks 是否移除超連結
* @return string MHTML 內容
*/
function htmlToWord(string $html, string $baseUrl = '', bool $stripLinks = true): string {
$mht = new MhtFileMaker();
// 1. 移除超連結(保留文字)
if ($stripLinks) {
$html = preg_replace('/<a\s[^>]*>(.*?)<\/a>/is', '$1', $html);
}
// 2. 提取所有圖片 src
$imageUrls = [];
$imageLocations = [];
if (preg_match_all('/<img[^>]+src\s*=\s*["\']([^"\']+)["\']/i', $html, $matches)) {
foreach ($matches[1] as $idx => $src) {
$src = trim($src);
if ($src === '' || str_starts_with($src, 'data:')) continue;
// 處理相對路徑
$fullUrl = $src;
if (!preg_match('/^https?:\/\//i', $src)) {
$fullUrl = rtrim($baseUrl, '/') . '/' . ltrim($src, '/');
}
// 產生唯一內部位置識別碼
$location = "image_{$idx}_" . basename($src);
$imageUrls[$src] = $fullUrl;
$imageLocations[$src] = $location;
// 替換 HTML 中的 src 為內部位置
$html = str_replace($src, $location, $html);
}
}
// 3. 加入主 HTML 部分
$mht->addPart('main.html', 'text/html', $html);
// 4. 下載並加入圖片
foreach ($imageUrls as $originalSrc => $fullUrl) {
$location = $imageLocations[$originalSrc];
$mimeType = $mht->getMimeType($location);
$ctx = stream_context_create([
'http' => ['timeout' => 10, 'user_agent' => 'Mozilla/5.0 PHP Word Export'],
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false], // 開發環境可關閉驗證
]);
$imgData = @file_get_contents($fullUrl, false, $ctx);
if ($imgData !== false) {
$mht->addPart($location, $mimeType, $imgData);
} else {
error_log("Word 匯出:無法下載圖片 {$fullUrl}");
}
}
return $mht->build();
}
<?php
require_once 'MhtFileMaker.php';
$html = '<h1>產品報價單</h1>
<p>日期:' . date('Y-m-d') . '</p>
<img src="/images/logo.png" alt="Logo">
<table border="1"><tr><th>品項</th><th>價格</th></tr>
<tr><td>商品 A</td><td>$100</td></tr></table>';
$wordContent = htmlToWord($html, 'https://example.com/');
// 直接下載
header('Content-Type: application/msword');
header('Content-Disposition: attachment; filename="quote_' . date('Ymd') . '.doc"');
header('Content-Length: ' . strlen($wordContent));
echo $wordContent;
exit;
// 或儲存檔案
// file_put_contents('storage/quote.doc', $wordContent);
<?php
// 在 htmlToWord() 中加入 CSS 處理
if (preg_match_all('/<link[^>]+rel=["\']stylesheet["\'][^>]+href=["\']([^"\']+)["\']/i', $html, $cssMatches)) {
foreach ($cssMatches[1] as $idx => $href) {
$fullCssUrl = $href;
if (!preg_match('/^https?:\/\//i', $href)) {
$fullCssUrl = rtrim($baseUrl, '/') . '/' . ltrim($href, '/');
}
$cssContent = @file_get_contents($fullCssUrl);
if ($cssContent) {
$location = "style_{$idx}.css";
$mht->addPart($location, 'text/css', $cssContent);
// 將 link 標籤替換為內嵌 style
$html = str_replace($cssMatches[0][$idx], "<style>{$cssContent}</style>", $html);
}
}
}
| 項目 | 說明 |
|---|---|
| 檔案大小 | Base64 編碼約增大 33%,大量圖片建議改用 PHPWord 產生真正的 .docx (OOXML) |
| 複雜排版 | MHTML 排版能力受限於 Word 的 HTML 渲染引擎,複雜表格、浮動、Flex/Grid 可能失效 |
| 中文檔名 | 下載時需 RFC 5987 編碼:filename*=UTF-8''%E6%8A%A5%E8%A1%A8.doc |
| 相容性 | Word 2007+ 完美支援,LibreOffice/OpenOffice 亦可開啟 |
| 安全性 | 嚴禁直接使用者輸入的 HTML 未經淨化,防 XSS 與 SSRF(file_get_contents 讀取內網) |
<?php
// composer require phpoffice/phpword
use PhpOffice\PhpWord\IOFactory;
use PhpOffice\PhpWord\TemplateProcessor;
// 範本式產生(推薦正式專案)
$template = new TemplateProcessor('template.docx');
$template->setValue('title', '報價單');
$template->setValue('date', date('Y-m-d'));
$template->save('output.docx');
PHPWord 產生真正的 OOXML 格式,支援完整 Word 功能(目錄、頁碼、樣式、圖表),是企業級專案的首選。