WMF support

Informations

Author: Martin Hall-May
License: 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();
?>
View the result here.

Download

ZIP | TGZ