Overview of MHT-Based Document Generation
Creating native Microsoft Word documents directly from PHP can be complex without heavy external libraries. A lightweight alternative involves generating files in the MHT (MIME HTML) format. Modern versions of Microsoft Word can open MHT files seamlessly, allowing developers to treat them as .doc files. This approach utilizes MIME multipart srtuctures to embed images and HTML content into a single file.
Core MIME Builder Class
The following class handles the construction of the MIME message. It manages boundaries, encodes content in Base64, and aggregates resources such as images and stylesheeets into the final package. The implementation has been modernized to use standard PHP 5+ constructor syntax and explicit visibility modifiers.
<?php
class MhtBuilder {
private $mimeHeaders = array();
private $headerMap = array();
private $resources = array();
private $boundaryId;
private $rootPath;
private $primaryPage;
public function __construct($settings = array()) {
// Initialization logic can be extended here
}
public function addHeader($headerLine) {
$this->mimeHeaders[] = $headerLine;
$key = strtolower(substr($headerLine, 0, strpos($headerLine, ':')));
$this->headerMap[$key] = true;
}
public function setSender($email) {
$this->addHeader("From: $email");
}
public function setTopic($topic) {
$this->addHeader("Subject: $topic");
}
public function setTime($timestamp = null, $isUnixTime = false) {
if ($timestamp === null) {
$timestamp = time();
}
if ($isUnixTime) {
$timestamp = date('D, d M Y H:i:s O', $timestamp);
}
$this->addHeader("Date: $timestamp");
}
public function defineBoundary($customBoundary = null) {
if ($customBoundary === null) {
$this->boundaryId = '--' . strtoupper(md5(mt_rand())) . '_MIXED_CONTENT';
} else {
$this->boundaryId = $customBoundary;
}
}
public function setRootDirectory($path) {
$this->rootPath = str_replace("\\", "/", realpath($path));
}
public function setEntryPage($fileName) {
$this->primaryPage = str_replace("\\", "/", realpath("{$this->rootPath}/$fileName"));
}
public function scanAndAddFiles() {
if (!isset($this->primaryPage)) {
throw new Exception('Entry page not defined.');
}
$relativePath = str_replace($this->rootPath, '', $this->primaryPage);
$virtualPath = 'http://mhtfile' . $relativePath;
$this->addResource($this->primaryPage, $virtualPath, null);
$this->recurseDirectory($this->rootPath);
}
private function recurseDirectory($dir) {
$handle = opendir($dir);
while ($file = readdir($handle)) {
if (($file != '.') && ($file != '..') && ("$dir/$file" != $this->primaryPage)) {
if (is_dir("$dir/$file")) {
$this->recurseDirectory("$dir/$file");
} elseif (is_file("$dir/$file")) {
$relativePath = str_replace($this->rootPath, '', "$dir/$file");
$virtualPath = 'http://mhtfile' . $relativePath;
$this->addResource("$dir/$file", $virtualPath, null);
}
}
}
closedir($handle);
}
public function addResource($filePath, $virtualPath = null, $encoding = null) {
if ($virtualPath === null) {
$virtualPath = $filePath;
}
$mime = $this->detectMimeType($filePath);
$data = file_get_contents($filePath);
$this->appendContent($virtualPath, $mime, $data, $encoding);
}
public function appendContent($path, $mime, $data, $encoding = null) {
if ($encoding === null) {
$data = chunk_split(base64_encode($data), 76);
$encoding = 'base64';
}
$this->resources[] = array(
'path' => $path,
'mime' => $mime,
'data' => $data,
'encoding' => $encoding
);
}
private function validateHeaders() {
if (!array_key_exists('date', $this->headerMap)) {
$this->setTime(null, true);
}
if ($this->boundaryId === null) {
$this->defineBoundary();
}
}
private function hasResources() {
return count($this->resources) > 0;
}
public function buildContent() {
$this->validateHeaders();
if (!$this->hasResources()) {
throw new Exception('No resources added to the document.');
}
$output = implode("\r\n", $this->mimeHeaders);
$output .= "\r\nMIME-Version: 1.0\r\n";
$output .= "Content-Type: multipart/related;\r\n";
$output .= "\tboundary=\"{$this->boundaryId}\";\r\n";
$output .= "\ttype=\"" . $this->resources[0]['mime'] . "\"\r\n";
$output .= "X-MimeOLE: Produced By MhtBuilder v1.0\r\n\r\n";
$output .= "This is a multi-part message in MIME format.\r\n\r\n";
foreach ($this->resources as $resource) {
$output .= "--{$this->boundaryId}\r\n";
$output .= "Content-Type: {$resource['mime']}\r\n";
$output .= "Content-Transfer-Encoding: {$resource['encoding']}\r\n";
$output .= "Content-Location: {$resource['path']}\r\n\r\n";
$output .= $resource['data'] . "\r\n";
}
$output .= "--{$this->boundaryId}--\r\n";
return $output;
}
public function saveToFile($destination) {
$content = $this->buildContent();
$handle = fopen($destination, 'w');
fwrite($handle, $content);
fclose($handle);
}
private function detectMimeType($filePath) {
$info = pathinfo($filePath);
$ext = isset($info['extension']) ? strtolower($info['extension']) : '';
$types = array(
'htm' => 'text/html', 'html' => 'text/html',
'txt' => 'text/plain', 'cgi' => 'text/plain',
'php' => 'text/plain', 'css' => 'text/css',
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg',
'jpe' => 'image/jpeg', 'gif' => 'image/gif',
'png' => 'image/png'
);
return isset($types[$ext]) ? $types[$ext] : 'application/octet-stream';
}
}
?>
HTML Processing Helper
To facilitate the creation of Word documents from dynamic HTML strings, a helper function is required. This function parses the HTML content, identifies image tags, resolves their paths (converting relative paths to absolute where necessary), and feeds everything into the MIME builder. It also offers an option to strip anchor links to clean up the document.
<?php
/**
* Converts HTML string into a Word-compatible MHT content stream.
*
* @param string $htmlBody The raw HTML content.
* @param string $baseUrl Base URL for resolving relative image paths.
* @param bool $stripLinks Whether to remove <a> tags from the content.
* @return string The generated MIME content.
*/
function buildWordContent($htmlBody, $baseUrl = "", $stripLinks = true) {
$builder = new MhtBuilder();
if ($stripLinks) {
// Remove anchor tags but keep inner text
$htmlBody = preg_replace('/<a\s*.*?\s>(\s*.*?\s*)<\/a>/i', '$1', $htmlBody);
}
$imageSources = array();
$filePaths = array();
$regexMatches = array();
// Extract src attributes from img tags (requires quoted values)
if (preg_match_all('/<img[.\n]*?src\s*?=\s*?[\"\'](.*?)[\"\'](.*?)\/>/i', $htmlBody, $regexMatches)) {
$rawUrls = $regexMatches[1];
for ($i = 0; $i < count($rawUrls); $i++) {
$src = trim($rawUrls[$i]);
if (!empty($src)) {
$filePaths[] = $src;
if (substr($src, 0, 7) !== 'http://') {
$src = $baseUrl . $src;
}
$imageSources[] = $src;
}
}
}
// Add the main HTML content
$builder->appendContent("index.html", $builder->detectMimeType("index.html"), $htmlBody);
// Add extracted images
for ($i = 0; $i < count($imageSources); $i++) {
$imgUrl = $imageSources[$i];
if (@fopen($imgUrl, 'r')) {
$imgData = @file_get_contents($imgUrl);
if ($imgData) {
$builder->appendContent($filePaths[$i], $builder->detectMimeType($imgUrl), $imgData);
}
} else {
error_log("Resource not found: " . $imgUrl);
}
}
return $builder->buildContent();
}
?>
Implementation Example
The following controller method demonstrates how to assemble an HTML table containing images and text, convert it using the helper function, and output it as a downloadable Word document. Character encoding is handled to ensure filenames display correctly in Windows environments.
<?php
public function exportReport() {
$htmlContent = "
<table width='600' cellpadding='6' cellspacing='1' bgcolor='#336699'>
<tr bgcolor='White'>
<td>Question ID: 1</td>
<td><img width='300px' src='http://www.example.com/assets/images/thumb/2023-11-24/sample_flower.jpg'></td>
</tr>
<tr>
<td>Solution:</td>
<td><img width='300px' src='http://www.example.com/assets/images/thumb/2023-11-24/sample_flower.jpg'></td>
</tr>
<tr bgcolor='White'>
<td>Question ID: 2</td>
<td><img width='300px' src='http://www.example.com/assets/images/thumb/2023-11-24/sample_flower.jpg'></td>
</tr>
<tr>
<td>Solution:</td>
<td><img width='300px' src='http://www.example.com/assets/images/thumb/2023-11-24/sample_flower.jpg'></td>
</tr>
</table>
";
$mimeData = buildWordContent($htmlContent);
$fileName = iconv("UTF-8", "GBK", 'Exam_Paper_Report');
header("Content-Type: application/msword");
header("Content-Disposition: attachment; filename=\"" . $fileName . ".doc\"");
echo $mimeData;
exit;
}
?>