Table des matières

Word document generation

This page explores the solutions to generate Word document on server side (eg. a webserver). My need is to generate full documents automatically which can later be edited by users, hence the choice of Word format.

Possible solutions

Why HTML ?

Trick: Embedding images requires external files. Thus we cannot use a single HTML file. The solution is simple: generate several files (the main document in html, images…) then put everything in a single MIME 1.0 file. This is exactly what MS Word does when you save as mhtml (.mht) documents. Header & footers also must be put in a separate file: They will also be included in the MIME file.

Generating MIME 1.0 files is easy, even in php.

So our solution can be sum up as:

Building a Word HTML document

Here are the different items you can/must use to build a Microsoft Office Word HTML document. These are only snippets, not full html documents. (Full HTML example documents will be in the "Examples" section.)

Basically, you generate a HTML document then serve it to the client as a .doc file (both in filename extension and in MIME type). Word will open this file as if it was a simple .doc file.

HTML declaration

<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
...
</html>

Document properties

<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
<head><title>Microsoft Office HTML Example</title>
<style> <!-- 
@page
{
    size: 21cm 29.7cm;  /* A4 */
    margin: 2cm 2cm 2cm 2cm; /* Margins: 2 cm on each side */
    mso-page-orientation: portrait;  
}
--></style>

Page declaration

You are supposed to put pages (or group of pages) in a "section" (in a DIV), like this:

<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
<head><title>Microsoft Office HTML Example</title>
<style><!-- 
@page
{
    size:21cm 29.7cmt;  /* A4 */
    margin:1cm 1cm 1cm 1cm; /* Margins: 2.5 cm on each side */
    mso-page-orientation: portrait;  
}
@page Section1 { }
div.Section1 { page:Section1; }
--></style>
</head>
<body>
<div class=Section1>
I'm page 1.
<br clear=all style='mso-special-character:line-break;page-break-before:always'>
I'm page 2.
</div>
</body>
</html>

Caveat: Changing things like page orientation for a group or a specific page does not seem to work. FIXME

Standard HTML/CSS elements

Word will accept most standard HTML and CSS features, such as headings (h1,h2,h3…), lists (ul,li), tables, colors… Go experiment yourself. Here are some examples:

basic_html.doc
<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
<head><title>Microsoft Office HTML Example</title></head>
<body>
<h1>Title level 1</h1>
<h2>Title level 2</h2>
<h3>Title level 3</h3>
<p>Text in level 3</p>
<h2>2nd title level 2</h2>
<h3>Another level 3 title</h3>
 
List:
<ul>
<li>element 1</li>
<li>element 2</li>
<li>element 3</li>
  <ul>
  <li>element 4</li>
  <li>element 5</li>
  <li>element 6</li>
      <ul>
      <li>element 7</li>
      <li>element 8</li>
      </ul>
  </ul>
<li>element 9</li>
<li>element 10</li>
</ul>
 
<table width="100%">
<thead style="background-color:#A0A0FF;"><td nowrap>Column A</td><td nowrap>Column B</td><td nowrap>Column C</td></thead>
<tr><td>A1</td><td>B1</td><td>C1</td></tr>
<tr><td>A2</td><td>B2 Test with looooong text: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed sapien 
ac tortor porttitor lobortis. Donec velit urna, vulputate eu egestas eu, lacinia non dolor. Cras lacus diam, tempus 
sed ullamcorper a, euismod id nunc. Fusce egestas velit sed est fermentum tempus. Duis sapien dui, consectetur eu 
accumsan id, tristique sit amet ante.</td><td>C2</td></tr>
<tr><td>A3</td><td>B3</td><td>C3</td></tr>
</table>
 
</body>
</html>

Rendering in Word:

Note that these elements can later be styled, either using inline CSS (<p style="…">) or using CSS stylesheets (eg. you can style all h1 elements).

Forcing display mode on opening

<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
<head><title>Microsoft Office HTML Example</title>
<!--[if gte mso 9]>
<xml>
<w:WordDocument>
<w:View>Print</w:View>
<w:Zoom>100</w:Zoom>
<w:DoNotOptimizeForBrowser/>
</w:WordDocument>
</xml>
<![endif]-->
<body>
...

This will force the "Page" display mode when the file is opened. This section must be put just after the title, otherwise it will not work. You can use 80 or 90 for Zoom if you want two pages to fit on screen.

Page break

<br clear=all style='mso-special-character:line-break;page-break-before:always'>

Tables and pagebreaks

Prevent a table cell from spanning over multiple pages

Put in your stylesheet:

td { page-break-inside:avoid; }

or apply to only specific cells:

<td style="page-break-inside:avoid;">...</td>

Prevent tables from spanning over multiple pages

Put in your stylesheet:

tr { page-break-after:avoid; }

or apply to all TR of a table:

<tr style="page-break-after:avoid;">...</tr>

A note about computed field

Computed fields include TOC (table of content), page refences and so on.

With Word, when you open a document, all computed fields are not updated by default. This has to be done manually by typing CTRL+A (to select the whole document) then press F9.

Thus, all computed fields you insert in your document will not show up unless the user manually updates them. This a problem related to Word itself. There is not simple solution to this.

TOC (Table of content)

<p class=MsoToc1> 
<!--[if supportFields]> 
<span style='mso-element:field-begin'></span> 
TOC \o "1-3" \u 
<span style='mso-element:field-separator'></span> 
<![endif]--> 
<span style='mso-no-proof:yes'>Table of content - Please right-clic and choose "Update fields".</span> 
<!--[if supportFields]> 
<span style='mso-element:field-end'></span> 
<![endif]--> 
</p>

As you can't predict the page numbers, the TOC needs to be manually updated by the user (Not a heavy burden, and I guess it's possible to automate this by including a script in the file. I'll investigate that later.)

TOC before update (upon file opening):

TOC after update: It reflects the different heading levels (h1,h2,h3…).

If you want to customize the TOC, have a look at the Microsoft documentation about this dynamic field.

Bookmarks and references

You can reference another chapter or page rather easily. Here is an example: Set a bookmark in a document, and display the page where this bookmark is located.

Bookmarks: Simply put a html anchor:

<a name="MyBookmark"></a>Appendix

Then the reference:

For more information, see appendix at page 
<!--[if supportFields]>
<span style='mso-element:field-begin'></span>PAGEREF MyBookmark \h <span style='mso-element:field-end'></span>
<![endif]-->

Headers and footers must be put in a separate file, in a subdirectory. Example:

Note: It is important that the subdirectory name starts with the main document name (mydocument.htm → mydocument_files), otherwise Word will display a warning.

mydocument.htm
<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
<head><title>Microsoft Office HTML Example</title>
<link rel=File-List href="mydocument_files/filelist.xml">
<style><!-- 
@page
{
    size:21cm 29.7cmt;  /* A4 */
    margin:1cm 1cm 1cm 1cm; /* Margins: 2.5 cm on each side */
    mso-page-orientation: portrait;  
    mso-header: url("mydocument_files/headerfooter.htm") h1;
    mso-footer: url("mydocument_files/headerfooter.htm") f1;	
}
@page Section1 { }
div.Section1 { page:Section1; }
p.MsoHeader, p.MsoFooter { border: 1px solid black; }
--></style>
</head>
<body>
<div class=Section1>
I'm page 1.
<br clear=all style='mso-special-character:line-break;page-break-before:always'>
I'm page 2.
</div>
</body>
</html>

Note that the file filelist.xml does not need to be present, but its declaration in the main document is mandatory.

mydocument_files\headerfooter.htm
<html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns:m="http://schemas.microsoft.com/office/2004/12/omml"= xmlns="http://www.w3.org/TR/REC-html40">
<body>
 
<div style="mso-element:header;" id="h1">
<p class=MsoHeader>Header</p>
</div>
 
<div style='mso-element:footer' id=f1>
<p class=MsoFooter><span class=SpellE>Footer</span> page <!--[if supportFields]><span
class=MsoPageNumber><span style='mso-element:field-begin'></span><span
style='mso-spacerun:yes'> </span>PAGE <span style='mso-element:field-separator'></span></span><![endif]--><span
class=MsoPageNumber><span style='mso-no-proof:yes'>1</span></span><!--[if supportFields]><span
class=MsoPageNumber><span style='mso-element:field-end'></span></span><![endif]--><span
class=MsoPageNumber>/</span><!--[if supportFields]><span class=MsoPageNumber><span
style='mso-element:field-begin'></span> NUMPAGES <span style='mso-element:field-separator'></span></span><![endif]--><span
class=MsoPageNumber><span style='mso-no-proof:yes'>1</span></span><!--[if supportFields]><span
class=MsoPageNumber><span style='mso-element:field-end'></span></span><![endif]-->
</p>
</div>
 
</body>
</html>

After opening the document, don't forget to go in "Page" display mode to see headers/footers.

This is what you get:

Images

As for the Header/Footer, images must be put in a subdirectory. Then you just use the standard <img> html tag. Example:

mydocument.htm
<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>
<head><title>Microsoft Office HTML Example</title>
<link rel=File-List href="mydocument_files/filelist.xml">
<style><!-- 
@page
{
    size:21cm 29.7cmt;  /* A4 */
    margin:1cm 1cm 1cm 1cm; /* Margins: 2.5 cm on each side */
    mso-page-orientation: portrait;  
}
@page Section1 { }
div.Section1 { page:Section1; }
p.MsoHeader, p.MsoFooter { border: 1px solid black; }
--></style>
</head>
<body>
<div class=Section1>
Here is an image:<br>
<img src="mydocument_files/logo_google.png">
</div>
</body>
</html>

Result in Word:

Styling

You can include a CSS stylesheet in the main html file: Word will use it.

You can style standard HTML elements (h1,h2,h3…), but you can also apply styles with the class attribute.

Exploring other Word documents features

If you are trying to find the HTML code corresponding to a Word feature, here's my advice:

Then open with you favorite text editor. You are most likely to find the relevant HTML/CSS code.

Creating a MIME (mhtml) file

Once you have created your html document with its associated files (headers/footer, images…), you need to pack them in a single mhtml (MIME) file.

Let's take an example: A document with header/footer and an image. The files contained in this zip (mime_example.zip) are:

Building a MIME file only requires to encode these files in base64 and add a header for each one:

mime_example.doc
MIME-Version: 1.0
Content-Type: multipart/related; boundary="----=_NextPart_ZROIIZO.ZCZYUACXV.ZARTUI"
 
------=_NextPart_ZROIIZO.ZCZYUACXV.ZARTUI
Content-Location: file:///C:/mydocument.htm
Content-Transfer-Encoding: base64
Content-Type: text/html; charset="utf-8"
 
PGh0bWwgeG1sbnM6bz0ndXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTpvZmZpY2U6b2ZmaWNlJyB4
bWxuczp3PSd1cm46c2NoZW1hcy1taWNyb3NvZnQtY29tOm9mZmljZTp3b3JkJyB4bWxucz0naHR0
cDovL3d3dy53My5vcmcvVFIvUkVDLWh0bWw0MCc+DQo8aGVhZD48dGl0bGU+TWljcm9zb2Z0IE9m
ZmljZSBIVE1MIEV4YW1wbGU8L3RpdGxlPg0KPCEtLVtpZiBndGUgbXNvIDldPg0KPHhtbD48dzpX
b3JkRG9jdW1lbnQ+PHc6Vmlldz5QcmludDwvdzpWaWV3Pjx3Olpvb20+MTAwPC93Olpvb20+PHc6
RG9Ob3RPcHRpbWl6ZUZvckJyb3dzZXIvPjwvdzpXb3JkRG9jdW1lbnQ+PC94bWw+DQo8IVtlbmRp
Zl0tLT4NCjxsaW5rIHJlbD1GaWxlLUxpc3QgaHJlZj0ibXlkb2N1bWVudF9maWxlcy9maWxlbGlz
dC54bWwiPg0KPHN0eWxlPjwhLS0gDQpAcGFnZQ0Kew0KICAgIHNpemU6MjFjbSAyOS43Y210OyAg
LyogQTQgKi8NCiAgICBtYXJnaW46MWNtIDFjbSAxY20gMWNtOyAvKiBNYXJnaW5zOiAyLjUgY20g
b24gZWFjaCBzaWRlICovDQogICAgbXNvLXBhZ2Utb3JpZW50YXRpb246IHBvcnRyYWl0OyAgDQoJ
bXNvLWhlYWRlcjogdXJsKCJteWRvY3VtZW50X2ZpbGVzL2hlYWRlcmZvb3Rlci5odG0iKSBoMTsN
Cgltc28tZm9vdGVyOiB1cmwoIm15ZG9jdW1lbnRfZmlsZXMvaGVhZGVyZm9vdGVyLmh0bSIpIGYx
OwkNCn0NCkBwYWdlIFNlY3Rpb24xIHsgfQ0KZGl2LlNlY3Rpb24xIHsgcGFnZTpTZWN0aW9uMTsg
fQ0KcC5Nc29IZWFkZXIsIHAuTXNvRm9vdGVyIHsgYm9yZGVyOiAxcHggc29saWQgYmxhY2s7IH0N
Ci0tPjwvc3R5bGU+DQo8L2hlYWQ+DQo8Ym9keT4NCjxkaXYgY2xhc3M9U2VjdGlvbjE+DQpJJ20g
cGFnZSAxIDxpbWcgc3JjPSJteWRvY3VtZW50X2ZpbGVzL3NtaWxleS5naWYiPg0KPGJyIGNsZWFy
PWFsbCBzdHlsZT0nbXNvLXNwZWNpYWwtY2hhcmFjdGVyOmxpbmUtYnJlYWs7cGFnZS1icmVhay1i
ZWZvcmU6YWx3YXlzJz4NCkknbSBwYWdlIDIuDQo8L2Rpdj4NCjwvYm9keT4NCjwvaHRtbD4NCg0K
DQo=
 
------=_NextPart_ZROIIZO.ZCZYUACXV.ZARTUI
Content-Location: file:///C:/mydocument_files/headerfooter.htm
Content-Transfer-Encoding: base64
Content-Type: text/html; charset="utf-8"
 
PGh0bWwgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVy
bjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSIgeG1sbnM6dz0idXJuOnNjaGVt
YXMtbWljcm9zb2Z0LWNvbTpvZmZpY2U6d29yZCIgeG1sbnM6bT0iaHR0cDovL3NjaGVtYXMubWlj
cm9zb2Z0LmNvbS9vZmZpY2UvMjAwNC8xMi9vbW1sIj0geG1sbnM9Imh0dHA6Ly93d3cudzMub3Jn
L1RSL1JFQy1odG1sNDAiPg0KPGJvZHk+DQoNCjxkaXYgc3R5bGU9Im1zby1lbGVtZW50OmhlYWRl
cjsiIGlkPSJoMSI+DQo8cCBjbGFzcz1Nc29IZWFkZXI+SGVhZGVyPC9wPg0KPC9kaXY+DQoNCjxk
aXYgc3R5bGU9J21zby1lbGVtZW50OmZvb3RlcicgaWQ9ZjE+DQo8cCBjbGFzcz1Nc29Gb290ZXI+
PHNwYW4gY2xhc3M9U3BlbGxFPkZvb3Rlcjwvc3Bhbj4gcGFnZSA8IS0tW2lmIHN1cHBvcnRGaWVs
ZHNdPjxzcGFuDQpjbGFzcz1Nc29QYWdlTnVtYmVyPjxzcGFuIHN0eWxlPSdtc28tZWxlbWVudDpm
aWVsZC1iZWdpbic+PC9zcGFuPjxzcGFuDQpzdHlsZT0nbXNvLXNwYWNlcnVuOnllcyc+oDwvc3Bh
bj5QQUdFIDxzcGFuIHN0eWxlPSdtc28tZWxlbWVudDpmaWVsZC1zZXBhcmF0b3InPjwvc3Bhbj48
L3NwYW4+PCFbZW5kaWZdLS0+PHNwYW4NCmNsYXNzPU1zb1BhZ2VOdW1iZXI+PHNwYW4gc3R5bGU9
J21zby1uby1wcm9vZjp5ZXMnPjE8L3NwYW4+PC9zcGFuPjwhLS1baWYgc3VwcG9ydEZpZWxkc10+
PHNwYW4NCmNsYXNzPU1zb1BhZ2VOdW1iZXI+PHNwYW4gc3R5bGU9J21zby1lbGVtZW50OmZpZWxk
LWVuZCc+PC9zcGFuPjwvc3Bhbj48IVtlbmRpZl0tLT48c3Bhbg0KY2xhc3M9TXNvUGFnZU51bWJl
cj4vPC9zcGFuPjwhLS1baWYgc3VwcG9ydEZpZWxkc10+PHNwYW4gY2xhc3M9TXNvUGFnZU51bWJl
cj48c3Bhbg0Kc3R5bGU9J21zby1lbGVtZW50OmZpZWxkLWJlZ2luJz48L3NwYW4+IE5VTVBBR0VT
IDxzcGFuIHN0eWxlPSdtc28tZWxlbWVudDpmaWVsZC1zZXBhcmF0b3InPjwvc3Bhbj48L3NwYW4+
PCFbZW5kaWZdLS0+PHNwYW4NCmNsYXNzPU1zb1BhZ2VOdW1iZXI+PHNwYW4gc3R5bGU9J21zby1u
by1wcm9vZjp5ZXMnPjE8L3NwYW4+PC9zcGFuPjwhLS1baWYgc3VwcG9ydEZpZWxkc10+PHNwYW4N
CmNsYXNzPU1zb1BhZ2VOdW1iZXI+PHNwYW4gc3R5bGU9J21zby1lbGVtZW50OmZpZWxkLWVuZCc+
PC9zcGFuPjwvc3Bhbj48IVtlbmRpZl0tLT4NCjwvcD4NCjwvZGl2Pg0KDQo8L2JvZHk+DQo8L2h0
bWw+
 
------=_NextPart_ZROIIZO.ZCZYUACXV.ZARTUI
Content-Location: file:///C:/mydocument_files/smiley.gif
Content-Transfer-Encoding: base64
Content-Type: image/gif
 
R0lGODlhEgASAOeQAEM0EP/lIf/mIv/jH//mI//kIP/lIvC2AN3e3f/kH9LQynBeMPvYD/zZEens
7/fLAPvXDffMAM7MxJ2Sd//iHeyrAOecAGBLF//kIfrTCOfq7dfX0//iHv7gG3dmO3hnPPC3AGRP
Hf3fGfjMAJCEY+miAMzKwfvXDl9JFPK9AKZxBKCXfP7hHaGXfffKAPbJAP3dF++yAO+xAPbIAJCD
YtfX0mhSHW9TB52TeG1NB8KLAmlTHvTEAK1+A//nI/7iHpySdt6uAWdQHK+KBPrSB+jr7vPBANeu
AtSmAvzbE/7iHXhmPG5OB8ukAuemAJ6UePvZEG5QB/bKAHFTB5hrBKZsA9+0AfjOAsqEAvzbFM6G
AZViBKyBBPPCAPPGAOyqAG9NB/TCAN+XAP7fGfG8AJKGZv7jH/7fGrt3Ap+VesKMAvTMBnZlOvTD
AO25ALGMA/3cFmBKFuaZAM+NAcvJwHVjN/LEAKRrBJpwBPjPBu2uAJ99BPK/AGxLB6+EA+qpALGN
A6h2BP7gGqVqA/XHAP3gGuOVAOCTAN+VAHNiNc3Lw3NXB8yLAvO/AGhRHfC4AP//////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////yH5BAEAAP8ALAAAAAASABIA
AAj+AP8JHNjiw4ULH1oMXDhwwo0hRwjxQMIlygSGAhfsWQOhQYMTRFy4wbOAoY0mEOCcoUChAwwG
I1Ko2TGQBCAIYzgMGFCgwABBDB6ACFRG4KI8MHQCKIAhAIABIjKE+cPkH443EDrwBBAggAGuLKC8
kKEiTR0rDShsNSBAAAADCZI8ODBnyQUvaZkGEECgr4AEWSIcEBMijp0TS73y9UFAgBkAUmIgCpEo
SIanTfn2DVAIQJsKWtgA8eOCwVPFAgIoARDhUYkqT/5NITPCNIsECX6IAHClUQVDYATS6JHiwWUA
yFnz0WNhEImBQnQcMDLjQYQXXUB8sYDGEcMFVJwiyDhwIEaFEoe2lMQ4IYcKRhbkYLnT5yLGgSs8
oEDhYQXGgAA7
 
------=_NextPart_ZROIIZO.ZCZYUACXV.ZARTUI--

Please note:

This file can be renamed to .doc and opened in Word:

Amusing fact: mhtml (MIME) documents are usually smaller than their true .doc counterparts. For example, the previous mime_example.doc (which is a MIME/mhtml file) is 5216 bytes long. Resaved in true .doc format, it's 24064 bytes.

A basic MIME 1.0 class helper

Here is a basic MIME 1.0 class which will help you generate the mhtml files:

class mime10class
{
    private $data;
    const boundary='----=_NextPart_ERTUP.EFETZ.FTYIIBVZR.EYUUREZ';
    function __construct() { $this->data="MIME-Version: 1.0\nContent-Type: multipart/related; boundary=\"".self::boundary."\"\n\n"; }
    public function addFile($filepath,$contenttype,$data)
    {
        $this->data = $this->data.'--'.self::boundary."\nContent-Location: file:///C:/".preg_replace('!\\\!', '/', $filepath)."\nContent-Transfer-Encoding: base64\nContent-Type: ".$contenttype."\n\n";
        $this->data = $this->data.base64_encode($data)."\n\n";
    }
    public function getFile() { return $this->data.'--'.self::boundary.'--'; }
}

It's rather simple: Add files with addFile(), then get the final mhtml/MIME1.0 document with getFile().

Example:

header('Content-Type: application/msword');
header('Content-disposition: filename=mydocument.doc')
$doc = New mime10class();
$doc->addFile('mydocument.htm','text/html; charset="utf-8"','Hello, world !');
$doc->addFile('subdir\anotherfile.htm','text/html; charset="utf-8"','Hi there.');
echo $doc->getFile();

Sending the file to the client

The .mht file must be served to the client with the following HTTP headers:

Content-Type: application/msword
Content-disposition: attachment; filename=myfile.doc

Yes, we use ".doc" in order not to confuse the final user. Word will recognize this is a .mht file and will open it accordingly.

Note that:

Content-disposition: attachment; filename=myfile.doc

will force download, but:

Content-disposition: filename=myfile.doc

will allow the user to choose between "open" or "save".

Examples

Full, working HTML documents which load correctly in Microsoft Word, using many Word features: Styling, tables, headers & footers, page format, table-of-content, images…

FIXME

php code examples

FIXME

Performances

I have implemented this system in a professional environment, and we are able generate a 70 pages database-driven dynamically-filled document with lots of tables and references in less than 5 seconds. (The document contains no images; The server runs Apache+php+Oracle. Document templates are written in Smarty.)

The performances are excellent, much better than what I expected.

Ideas worth pondering