Add-On WMF support
Informations
Author:Martin Hall-MayLicense: FPDF
Description
This script allows a Windows Metafile (WMF) image to be imported into the PDF.The function ImageWMF() takes the same parameters as Image(). It scales and places the image appropriately on the page.
WMF files contain vector data described in terms of Windows Graphics Device Interface (GDI) commands. There are approximately 80 different GDI commands allowed for in the WMF standard. This script interprets only a small subset of these, but is sufficient to display most WMF images. Please feel free to add further functionality.
Note: only little-endian architectures (typically Intel processors) are supported.
Source
<?php
/****************************************************************************
* Software: FPDF_WMF *
* Version: 0.5 *
* Date: 2019-08-10 *
* Author: Martin HALL-MAY *
* License: FPDF *
****************************************************************************/
require('fpdf.php');
class FPDF_WMF extends FPDF
{
protected $formobjects=array(); // array of Form Objects
protected $gdiObjectArray; // array of GDI objects (pens, brushes, etc.)
// Play back a WMF file
// Placement and sizing of the image works as with Image()
function ImageWMF($file, $x, $y, $w=0, $h=0, $link='')
{
// Put a WMF image on the page
if(!isset($this->formobjects[$file]))
{
// First use of image, get info
$info=$this->_parsewmf($file);
$info['i']=count($this->formobjects)+1;
$this->formobjects[$file]=$info;
}
else
{
$info=$this->formobjects[$file];
}
// Automatic dimensions if necessary
// WMF units are twips (1/20pt)
// divide by 20 to get points
// divide by k to get user units
if($w==0 && $h==0)
{
$w = abs($info['w'])/(20*$this->k);
$h = abs($info['h'])/(20*$this->k);
}
if($w==0)
$w = abs($h*$info['w']/$info['h']);
if($h==0)
$h = abs($w*$info['h']/$info['w']);
$sx = $w*$this->k / $info['w'];
$sy = -$h*$this->k / $info['h'];
$this->_out(sprintf('q %F 0 0 %F %F %F cm /FO%d Do Q', $sx, $sy, $x*$this->k-$sx*$info['x'], (($this->h-$y)*$this->k)-$sy*$info['y'], $info['i']));
if($link)
$this->Link($x, $y, $w, $h, $link);
}
function _parsewmf($file)
{
$this->gdiObjectArray = array();
$a=unpack('stest', "\1\0");
if ($a['test']!=1)
$this->Error('Big-endian architectures are not supported');
$f=fopen($file, 'rb');
if(!$f)
$this->Error('Can\'t open image file: '.$file);
// check for Aldus placeable metafile header
$key = unpack('Lmagic', fread($f, 4));
$headSize = 18 - 4; // WMF header minus four bytes already read
if ($key['magic'] == (int)0x9AC6CDD7)
$headSize += 22; // Aldus header
// strip headers
fread($f, $headSize);
// define some state variables
$wo=null; // window origin
$we=null; // window extent
$polyFillMode = 0;
$nullPen = false;
$nullBrush = false;
$endRecord = false;
$data = '';
// read the records
while (!feof($f) && !$endRecord)
{
$recordInfo = unpack('Lsize/Sfunc', fread($f, 6));
// size of record given in WORDs (= 2 bytes)
$size = $recordInfo['size'];
// func is number of GDI function
$func = $recordInfo['func'];
// parameters are read as one block and processed
// as necessary by the case statement below.
// the data are stored in little-endian format and are unpacked using:
// s - signed 16-bit int
// S - unsigned 16-bit int (or WORD)
// L - unsigned 32-bit int (or DWORD)
// NB. parameters to GDI functions are stored in reverse order
// however structures are not reversed,
// e.g. POINT { int x, int y } where x=3000 (0x0BB8) and y=-1200 (0xFB50)
// is stored as B8 0B 50 FB
if ($size > 3)
{
$parms = fread($f, 2*($size-3));
}
// process each record.
// function numbers are defined in wingdi.h
switch ($func)
{
case 0x020b: // SetWindowOrg
// do not allow window origin to be changed
// after drawing has begun
if (!$data)
$wo = array_reverse(unpack('s2', $parms));
break;
case 0x020c: // SetWindowExt
// do not allow window extent to be changed
// after drawing has begun
if (!$data)
$we = array_reverse(unpack('s2', $parms));
break;
case 0x02fc: // CreateBrushIndirect
$brush = unpack('sstyle/Cr/Cg/Cb/Ca/Shatch', $parms);
$brush['type'] = 'B';
$this->_AddGDIObject($brush);
break;
case 0x02fa: // CreatePenIndirect
$pen = unpack('Sstyle/swidth/sdummy/Cr/Cg/Cb/Ca', $parms);
// convert width from twips to user unit
$pen['width'] /= (20 * $this->k);
$pen['type'] = 'P';
$this->_AddGDIObject($pen);
break;
// MUST create other GDI objects even if we don't handle them
// otherwise object numbering will get out of sequence
case 0x06fe: // CreateBitmap
case 0x02fd: // CreateBitmapIndirect
case 0x00f8: // CreateBrush
case 0x02fb: // CreateFontIndirect
case 0x00f7: // CreatePalette
case 0x01f9: // CreatePatternBrush
case 0x06ff: // CreateRegion
case 0x0142: // DibCreatePatternBrush
$dummyObject = array('type'=>'D');
$this->_AddGDIObject($dummyObject);
break;
case 0x0106: // SetPolyFillMode
$polyFillMode = unpack('smode', $parms);
$polyFillMode = $polyFillMode['mode'];
break;
case 0x01f0: // DeleteObject
$idx = unpack('Sidx', $parms);
$idx = $idx['idx'];
$this->_DeleteGDIObject($idx);
break;
case 0x012d: // SelectObject
$idx = unpack('Sidx', $parms);
$idx = $idx['idx'];
$obj = $this->_GetGDIObject($idx);
switch ($obj['type'])
{
case 'B':
$nullBrush = false;
if ($obj['style'] == 1) // BS_NULL, BS_HOLLOW
{
$nullBrush = true;
}
else
{
$data .= sprintf("%.3F %.3F %.3F rg\n", $obj['r']/255, $obj['g']/255, $obj['b']/255);
}
break;
case 'P':
$nullPen = false;
$dashArray = array();
// dash parameters are my own - feel free to change them
switch ($obj['style'])
{
case 0: // PS_SOLID
break;
case 1: // PS_DASH
$dashArray = array(3, 1);
break;
case 2: // PS_DOT
$dashArray = array(0.5, 0.5);
break;
case 3: // PS_DASHDOT
$dashArray = array(2, 1, 0.5, 1);
break;
case 4: // PS_DASHDOTDOT
$dashArray = array(2, 1, 0.5, 1, 0.5, 1);
break;
case 5: // PS_NULL
$nullPen = true;
break;
}
if (!$nullPen)
{
$data .= sprintf("%.3F %.3F %.3F RG\n", $obj['r']/255, $obj['g']/255, $obj['b']/255);
$data .= sprintf("%.2F w\n", $obj['width']*$this->k);
}
if (!empty($dashArray))
{
$s = '[';
for ($i=0; $i<count($dashArray);$i++)
{
$s .= $dashArray[$i] * $this->k;
if ($i != count($dashArray)-1)
$s .= ' ';
}
$s .= '] 0 d';
$data .= $s."\n";
}
break;
}
break;
case 0x0325: // Polyline
case 0x0324: // Polygon
$coords = unpack('s'.($size-3), $parms);
$numpoints = $coords[1];
for ($i = $numpoints; $i > 0; $i--)
{
$px = $coords[2*$i];
$py = $coords[2*$i+1];
if ($i < $numpoints)
$data .= $this->LineTo($px, $py);
else
$data .= $this->MoveTo($px, $py);
}
if ($func == 0x0325)
{
$op = 's';
}
else if ($func == 0x0324)
{
if ($nullPen)
{
if ($nullBrush)
$op = 'n'; // no op
else
$op = 'f'; // fill
}
else
{
if ($nullBrush)
$op = 's'; // stroke
else
$op = 'b'; // stroke and fill
}
if ($polyFillMode==1 && ($op=='b' || $op=='f'))
$op .= '*'; // use even-odd fill rule
}
$data .= $op."\n";
break;
case 0x0538: // PolyPolygon
$coords = unpack('s'.($size-3), $parms);
$numpolygons = $coords[1];
$adjustment = $numpolygons;
for ($j = 1; $j <= $numpolygons; $j++)
{
$numpoints = $coords[$j + 1];
for ($i = $numpoints; $i > 0; $i--)
{
$px = $coords[2*$i + $adjustment];
$py = $coords[2*$i+1 + $adjustment];
if ($i == $numpoints)
$data .= $this->MoveTo($px, $py);
else
$data .= $this->LineTo($px, $py);
}
$adjustment += $numpoints * 2;
}
if ($nullPen)
{
if ($nullBrush)
$op = 'n'; // no op
else
$op = 'f'; // fill
}
else
{
if ($nullBrush)
$op = 's'; // stroke
else
$op = 'b'; // stroke and fill
}
if ($polyFillMode==1 && ($op=='b' || $op=='f'))
$op .= '*'; // use even-odd fill rule
$data .= $op."\n";
break;
case 0x0000:
$endRecord = true;
break;
}
}
fclose($f);
return array('x'=>$wo[0], 'y'=>$wo[1], 'w'=>$we[0], 'h'=>$we[1], 'data'=>$data);
}
function MoveTo($x, $y)
{
return "$x $y m\n";
}
// a line must have been started using MoveTo() first
function LineTo($x, $y)
{
return "$x $y l\n";
}
function _AddGDIObject($obj)
{
// find next available slot
$idx = 0;
if (!empty($this->gdiObjectArray))
{
$empty = false;
$i = 0;
while (!$empty)
{
$empty = !isset($this->gdiObjectArray[$i]);
$i++;
}
$idx = $i-1;
}
$this->gdiObjectArray[$idx] = $obj;
}
function _GetGDIObject($idx)
{
return $this->gdiObjectArray[$idx];
}
function _DeleteGDIObject($idx)
{
unset($this->gdiObjectArray[$idx]);
}
function _putformobjects()
{
foreach($this->formobjects as $file=>$info)
{
$this->_newobj();
$this->formobjects[$file]['n']=$this->n;
$this->_put('<</Type /XObject');
$this->_put('/Subtype /Form');
$this->_put('/BBox ['.$info['x'].' '.$info['y'].' '.($info['w']+$info['x']).' '.($info['h']+$info['y']).']');
if ($this->compress)
$this->_put('/Filter /FlateDecode');
$data=($this->compress) ? gzcompress($info['data']) : $info['data'];
$this->_put('/Length '.strlen($data).'>>');
$this->_putstream($data);
unset($this->formobjects[$file]['data']);
$this->_put('endobj');
}
}
function _putxobjectdict()
{
parent::_putxobjectdict();
foreach($this->formobjects as $formobject)
$this->_put('/FO'.$formobject['i'].' '.$formobject['n'].' 0 R');
}
function _putresources()
{
$this->_putformobjects();
parent::_putresources();
}
}
?>
Example
<?php
require('fpdf-wmf.php');
$pdf = new FPDF_WMF();
$pdf->AddPage();
$pdf->ImageWMF('ringmaster.wmf', 55, 10, 110);
$pdf->Output();
?>