首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 導出 Word 文檔實戰:MHT 格式封裝圖片、樣式與跨平台兼容方案

     編輯:PHP 相關知識

前言

PHP 導出 Word (.doc) 主流方案有三種:COM 組件 (Windows Only、需 Office、性能差)、第三方庫 (PhpOffice/PHPWord 功能全但學習成本高)、**MHT 格式偽裝 .doc (無依賴、純 PHP、原生支持圖片/樣式、Word 完美打開)**。本文深度解析 MHT 方案原理、完整實現代碼、圖片/Base64 封裝、CSS 樣式保留、大文件性能優化與避坑指南。

為什麼選 MHT (MIME HTML / .mht)

  • 單文件封裝:HTML + 圖片 + CSS + JS 全部 Base64 編碼內嵌於一個 MIME 多部分消息中。
  • Word 原生支持:Word 2003+ 可直接打開 .mht/.doc (改後綴),完美渲染圖片、表格、樣式。
  • 零依賴:純 PHP 實現,無需安裝 Office、LibreOffice、Java、Python、COM、扩展。
  • 跨平台:Linux/Windows/macOS 同樣運行,Docker 容器化無障礙。
  • 模板靈活:直接輸出 HTML 模板,前端開發者可預覽調試,所見即所得。

MHT 文件結構 (RFC 2557)

From: <sender@example.com>
Subject: 導出文檔
Date: Mon, 15 Jan 2024 10:00:00 +0800
MIME-Version: 1.0
Content-Type: multipart/related;
	boundary="----BOUNDARY_123456";
	type="text/html"

This is a multi-part message in MIME format.

------BOUNDARY_123456
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: base64
Content-Location: index.html

PGh0bWw+... (Base64 編碼的 HTML 內容)

------BOUNDARY_123456
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-Location: logo.png

iVBORw0KGgoAAAANSUhEUg... (Base64 編碼的圖片數據)

------BOUNDARY_123456--

核心類:MhtFileMaker (完整可用)

<?php
class MhtFileMaker {
    private $headers = [];
    private $headersMap = [];
    private $parts = []; // ['location' => '', 'mime' => '', 'content' => '', 'encoding' => 'base64']
    private $boundary;
    private $baseDir;
    private $firstPage;
    
    public function __construct() {
        $this->boundary = '--' . strtoupper(md5(mt_rand() . microtime())) . '_MHT';
        $this->setHeader('MIME-Version', '1.0');
        $this->setHeader('Date', date('D, d M Y H:i:s O'));
    }
    
    public function setHeader(string $name, string $value): void {
        $this->headers[] = "$name: $value";
        $this->headersMap[strtolower($name)] = $value;
    }
    
    public function addPart(string $location, string $mimeType, string $content, string $encoding = 'base64'): void {
        // Base64 編碼並按 76 字符換行 (RFC 2045)
        if ($encoding === 'base64') {
            $content = chunk_split(base64_encode($content), 76);
        }
        $this->parts[] = [
            'location' => $location,
            'mime' => $mimeType,
            'content' => $content,
            'encoding' => $encoding,
        ];
    }
    
    // 便捷:添加 HTML 主體
    public function addHtml(string $html, string $location = 'index.html'): void {
        $this->addPart($location, 'text/html; charset=utf-8', $html);
        if (!$this->firstPage) $this->firstPage = $location;
    }
    
    // 便捷:添加圖片/資源 (自動 Base64)
    public function addResource(string $filePath, string $location = ''): bool {
        if (!is_file($filePath) || !is_readable($filePath)) return false;
        $location = $location ?: basename($filePath);
        $mime = $this->getMimeType($filePath);
        $content = file_get_contents($filePath);
        $this->addPart($location, $mime, $content);
        return true;
    }
    
    // 從 HTML 解析並內嵌圖片 (核心功能)
    public function embedImagesFromHtml(string &$html, string $baseUrl = ''): int {
        $count = 0;
        $dom = new DOMDocument();
        @$dom->loadHTML('' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        
        $xpath = new DOMXPath($dom);
        $images = $xpath->query('//img[@src]');
        
        foreach ($images as $img) {
            $src = $img->getAttribute('src');
            if (empty($src) || str_starts_with($src, 'data:')) continue; // 已是 Data URI
            
            $absoluteUrl = $baseUrl ? rtrim($baseUrl, '/') . '/' . ltrim($src, '/') : $src;
            $content = @file_get_contents($absoluteUrl);
            if ($content === false) continue; // 下載失敗跳過
            
            $mime = $this->getMimeType($absoluteUrl);
            $b64 = 'data:' . $mime . ';base64,' . base64_encode($content);
            $img->setAttribute('src', $b64); // 替換為 Data URI
            $count++;
        }
        
        // 保存修改後的 HTML (去除 DOMDocument 自動添加的 html/body 標籤)
        $newHtml = '';
        foreach ($dom->getElementsByTagName('body')->item(0)->childNodes as $node) {
            $newHtml .= $dom->saveHTML($node);
        }
        $html = $newHtml ?: $html; // 解析失敗保留原文
        
        return $count;
    }
    
    public function build(): string {
        $output = implode("\r\n", $this->headers) . "\r\n";
        $output .= "Content-Type: multipart/related;\r\n";
        $output .= "\tboundary=\"{$this->boundary}\";\r\n";
        $output .= "\ttype=\"text/html\"\r\n\r\n";
        $output .= "This is a multi-part message in MIME format.\r\n\r\n";
        
        foreach ($this->parts as $part) {
            $output .= "{$this->boundary}\r\n";
            $output .= "Content-Type: {$part['mime']}\r\n";
            $output .= "Content-Transfer-Encoding: {$part['encoding']}\r\n";
            $output .= "Content-Location: {$part['location']}\r\n\r\n";
            $output .= $part['content'] . "\r\n";
        }
        
        $output .= "{$this->boundary}--\r\n";
        return $output;
    }
    
    public function save(string $filename): void {
        file_put_contents($filename, $this->build());
    }
    
    private function getMimeType(string $path): string {
        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        return match ($ext) {
            'htm', 'html' => 'text/html',
            'txt' => 'text/plain',
            'css' => 'text/css',
            'js' => 'application/javascript',
            'jpg', 'jpeg', 'jpe' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
            'webp' => 'image/webp',
            'svg' => 'image/svg+xml',
            'pdf' => 'application/pdf',
            'doc', 'docx' => 'application/msword',
            'xls', 'xlsx' => 'application/vnd.ms-excel',
            'zip' => 'application/zip',
            default => 'application/octet-stream',
        };
    }
}

導出 Word 完整流程示例

<?php
require 'MhtFileMaker.php';

// 1. 準備 HTML 內容 (可來自模板引擎 Blade/Twig/Smarty)
$html = '<h1 style="color:#2c3e50">項目報告</h1>';
$html .= '<p>生成時間: ' . date('Y-m-d H:i:s') . '</p>';
$html .= '<img src="https://example.com/logo.png" alt="Logo" style="max-width:200px">';
$html .= '<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;width:100%">';
$html .= '<tr style="background:#34495e;color:white"><th>項目</th><th>進度</th><th>負責人</th></tr>';
$html .= '<tr><td>需求分析</td><td>100%</td><td>張三</td></tr>';
$html .= '<tr><td>開發編碼</td><td>80%</td><td>李四</td></tr>';
$html .= '</table>';

// 2. 創建 MHT 並內嵌圖片
$mht = new MhtFileMaker();
$mht->setHeader('Subject', '項目周報_' . date('Ymd'));
$mht->setHeader('From', 'system@example.com');

// 關鍵:解析 HTML 中的圖片並轉為 Data URI (自動下載、Base64、替換 src)
$mht->embedImagesFromHtml($html, 'https://example.com'); // baseUrl 用於補全相對路徑

// 添加 HTML 主體
$mht->addHtml($html);

// 可選:額外添加 CSS/JS/字體等資源 (若 HTML 中引用外部文件)
// $mht->addResource('/path/to/style.css', 'style.css');

// 3. 輸出為 .doc 文件 (瀏覽器下載)
$filename = '報告_' . date('Ymd_His') . '.doc';
header('Content-Type: application/msword');
header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
header('Content-Length: ' . strlen($mht->build()));
echo $mht->build();
exit;

進階:CSS 樣式內嵌與複雜佈局支持

// 1. 外部 CSS 內嵌到 style 標籤
function inlineCss(string $html, string $cssUrl): string {
    $css = @file_get_contents($cssUrl);
    if ($css) {
        $html = str_replace('', "", $html);
    }
    return $html;
}

// 2. 表格樣式增強 (Word 對 CSS 支持有限,建議內聯 style)
$html = preg_replace('/]*)>/i', '', $html);
$html = preg_replace('/]*)>/i', '', $html);
$html = preg_replace('/]*)>/i', '', $html);

// 3. 分頁符 (Word 支持)
$html .= '
'; // 強制分頁 $html .= '
'; // 後分頁 // 4. 頁眉頁腳 (MHT 方案受限,建議導出後 Word 手動添加或用 PHPWord)

大文件性能優化

問題解決方案
內存峰值過高 (大圖片/長文檔)分塊讀取文件 Base64 編碼,避免 file_get_contents 一次性載入大文件;或臨時文件落地流式輸出
Base64 體積膨脹 33%壓縮圖片 (WebP、降低質量)、移除冗餘資源、考慮 PHPWord 僅存引用圖片路徑 (需 Word 打開時聯網)
並發導出阻塞 PHP-FPM異步隊列:Web 請求寫入任務表,Worker 進程後台生成 MHT 存儲 OSS/本地,返回下載鏈接
超大報表 (超 50MB)分卷導出 (多個 .doc)、改用 PHPWord 流式寫入 .docx (XML 壓縮)、或導出 PDF (tcpdf/dompdf)

避坑指南

  1. HTML 編碼:務必確保 HTML 為 UTF-8,``,DOMDocument 解析時加 `` 前綴。
  2. 圖片跨域/防盜鏈:下載失敗時保留原 `src` 或顯示佔位圖,記錄日志告警。
  3. 相對路徑補全:`embedImagesFromHtml($html, $baseUrl)` 自動處理 `/img/a.png` → `$baseUrl/img/a.png`。
  4. Word 兼容性:避免 Flex/Grid/浮動佈局,使用 `` 佈局;內聯 `style` 屬性;測試 Word 2010/2016/365/WPS/LibreOffice。
  5. 文件名編碼:`rawurlencode($filename)` 解決中文文件名下載亂碼。
  6. 超時設置:`set_time_limit(300)`,大文檔生成需較長時間。
  7. 方案對比總結

    方案依賴圖片支持樣式支持性能跨平台維護成本推薦場景
    MHT 偽裝 .doc無✅ Base64 內嵌✅ 內聯 CSS/Table⚠️ 大文件內存壓力✅ 完美低輕量導出、報表、合同、無依賴環境
    PHPWord (PhpOffice)Composer✅ 內嵌/引用✅ 豐富 API中等✅中複雜文檔、需程序化控制結構、長期維護項目
    COM (Windows+Office)Office + COM✅ 原生✅ 完美極差 (進程啟動)❌ 僅 Windows高極少數需 Word 高級功能 (郵件合併、修訂、宏) 的舊系統
    LibreOffice Headless 轉換LibreOffice✅✅慢 (進程轉換)✅中批量格式轉換 (docx→pdf、html→docx)
    HTML 直接輸出 .doc無❌ 僅外鏈⚠️ 有限最快✅極低純文本、無圖片、臨時預覽

    總結

    MHT 方案是 **PHP 導出 Word 最性價比、零依賴、跨平台** 的解決方案。核心優勢在於 **HTML 模板開發體驗佳 + Base64 內嵌圖片單文件分發 + Word 原生打開**。配合 `MhtFileMaker` 類與 `embedImagesFromHtml()` 自動化圖片內嵌,可快速構建導出功能。生產環境注意:**大文件異步化、圖片壓縮、樣式內聯、兼容性測試**。長期項目建議遷移至 **PHPWord** 獲得更強的文檔結構控制能力。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved