1<?php
2
3namespace Mpdf\Image;
4
5use Mpdf\Color\ColorConverter;
6use Mpdf\Mpdf;
7
8class Wmf
9{
10
11	/**
12	 * @var \Mpdf\Mpdf
13	 */
14	private $mpdf;
15
16	/**
17	 * @var \Mpdf\Color\ColorConverter
18	 */
19	private $colorConverter;
20
21	/**
22	 * @var array
23	 */
24	private $gdiObjectArray;
25
26	public function __construct(Mpdf $mpdf, ColorConverter $colorConverter)
27	{
28		$this->mpdf = $mpdf;
29		$this->colorConverter = $colorConverter;
30	}
31
32	function _getWMFimage($data)
33	{
34		$k = Mpdf::SCALE;
35
36		$this->gdiObjectArray = [];
37		$a = unpack('stest', "\1\0");
38		if ($a['test'] != 1) {
39			return [0, 'Error parsing WMF image - Big-endian architecture not supported'];
40		}
41		// check for Aldus placeable metafile header
42		$key = unpack('Lmagic', substr($data, 0, 4));
43		$p = 18;  // WMF header
44		if ($key['magic'] == (int) 0x9AC6CDD7) {
45			$p +=22;
46		} // Aldus header
47		// define some state variables
48		$wo = null; // window origin
49		$we = null; // window extent
50		$polyFillMode = 0;
51		$nullPen = false;
52		$nullBrush = false;
53		$endRecord = false;
54		$wmfdata = '';
55		while ($p < strlen($data) && !$endRecord) {
56			$recordInfo = unpack('Lsize/Sfunc', substr($data, $p, 6));
57			$p += 6;
58			// size of record given in WORDs (= 2 bytes)
59			$size = $recordInfo['size'];
60			// func is number of GDI function
61			$func = $recordInfo['func'];
62			if ($size > 3) {
63				$parms = substr($data, $p, 2 * ($size - 3));
64				$p += 2 * ($size - 3);
65			}
66			switch ($func) {
67				case 0x020b:  // SetWindowOrg
68					// do not allow window origin to be changed
69					// after drawing has begun
70					if (!$wmfdata) {
71						$wo = array_reverse(unpack('s2', $parms));
72					}
73					break;
74				case 0x020c:  // SetWindowExt
75					// do not allow window extent to be changed
76					// after drawing has begun
77					if (!$wmfdata) {
78						$we = array_reverse(unpack('s2', $parms));
79					}
80					break;
81				case 0x02fc:  // CreateBrushIndirect
82					$brush = unpack('sstyle/Cr/Cg/Cb/Ca/Shatch', $parms);
83					$brush['type'] = 'B';
84					$this->_AddGDIObject($brush);
85					break;
86				case 0x02fa:  // CreatePenIndirect
87					$pen = unpack('Sstyle/swidth/sdummy/Cr/Cg/Cb/Ca', $parms);
88					// convert width from twips to user unit
89					$pen['width'] /= (20 * $k);
90					$pen['type'] = 'P';
91					$this->_AddGDIObject($pen);
92					break;
93
94				// MUST create other GDI objects even if we don't handle them
95				case 0x06fe: // CreateBitmap
96				case 0x02fd: // CreateBitmapIndirect
97				case 0x00f8: // CreateBrush
98				case 0x02fb: // CreateFontIndirect
99				case 0x00f7: // CreatePalette
100				case 0x01f9: // CreatePatternBrush
101				case 0x06ff: // CreateRegion
102				case 0x0142: // DibCreatePatternBrush
103					$dummyObject = ['type' => 'D'];
104					$this->_AddGDIObject($dummyObject);
105					break;
106				case 0x0106:  // SetPolyFillMode
107					$polyFillMode = unpack('smode', $parms);
108					$polyFillMode = $polyFillMode['mode'];
109					break;
110				case 0x01f0:  // DeleteObject
111					$idx = unpack('Sidx', $parms);
112					$idx = $idx['idx'];
113					$this->_DeleteGDIObject($idx);
114					break;
115				case 0x012d:  // SelectObject
116					$idx = unpack('Sidx', $parms);
117					$idx = $idx['idx'];
118					$obj = $this->_GetGDIObject($idx);
119					switch ($obj['type']) {
120						case 'B':
121							$nullBrush = false;
122							if ($obj['style'] == 1) {
123								$nullBrush = true;
124							} else {
125								$wmfdata .= $this->mpdf->SetFColor($this->colorConverter->convert('rgb(' . $obj['r'] . ',' . $obj['g'] . ',' . $obj['b'] . ')', $this->mpdf->PDFAXwarnings), true) . "\n";
126							}
127							break;
128						case 'P':
129							$nullPen = false;
130							$dashArray = [];
131							// dash parameters are custom
132							switch ($obj['style']) {
133								case 0: // PS_SOLID
134									break;
135								case 1: // PS_DASH
136									$dashArray = [3, 1];
137									break;
138								case 2: // PS_DOT
139									$dashArray = [0.5, 0.5];
140									break;
141								case 3: // PS_DASHDOT
142									$dashArray = [2, 1, 0.5, 1];
143									break;
144								case 4: // PS_DASHDOTDOT
145									$dashArray = [2, 1, 0.5, 1, 0.5, 1];
146									break;
147								case 5: // PS_NULL
148									$nullPen = true;
149									break;
150							}
151							if (!$nullPen) {
152								$wmfdata .= $this->mpdf->SetDColor($this->colorConverter->convert('rgb(' . $obj['r'] . ',' . $obj['g'] . ',' . $obj['b'] . ')', $this->mpdf->PDFAXwarnings), true) . "\n";
153								$wmfdata .= sprintf("%.3F w\n", $obj['width'] * $k);
154							}
155							if (!empty($dashArray)) {
156								$s = '[';
157								for ($i = 0; $i < count($dashArray); $i++) {
158									$s .= $dashArray[$i] * $k;
159									if ($i != count($dashArray) - 1) {
160										$s .= ' ';
161									}
162								}
163								$s .= '] 0 d';
164								$wmfdata .= $s . "\n";
165							}
166							break;
167					}
168					break;
169				case 0x0325: // Polyline
170				case 0x0324: // Polygon
171					$coords = unpack('s' . ($size - 3), $parms);
172					$numpoints = $coords[1];
173					for ($i = $numpoints; $i > 0; $i--) {
174						$px = $coords[2 * $i];
175						$py = $coords[2 * $i + 1];
176
177						if ($i < $numpoints) {
178							$wmfdata .= $this->_LineTo($px, $py);
179						} else {
180							$wmfdata .= $this->_MoveTo($px, $py);
181						}
182					}
183					if ($func == 0x0325) {
184						$op = 's';
185					} elseif ($func == 0x0324) {
186						if ($nullPen) {
187							if ($nullBrush) {
188								$op = 'n';
189							} // no op
190							else {
191								$op = 'f';
192							} // fill
193						} else {
194							if ($nullBrush) {
195								$op = 's';
196							} // stroke
197							else {
198								$op = 'b';
199							} // stroke and fill
200						}
201						if ($polyFillMode == 1 && ($op == 'b' || $op == 'f')) {
202							$op .= '*';
203						} // use even-odd fill rule
204					}
205					$wmfdata .= $op . "\n";
206					break;
207				case 0x0538: // PolyPolygon
208					$coords = unpack('s' . ($size - 3), $parms);
209					$numpolygons = $coords[1];
210					$adjustment = $numpolygons;
211					for ($j = 1; $j <= $numpolygons; $j++) {
212						$numpoints = $coords[$j + 1];
213						for ($i = $numpoints; $i > 0; $i--) {
214							$px = $coords[2 * $i + $adjustment];
215							$py = $coords[2 * $i + 1 + $adjustment];
216							if ($i == $numpoints) {
217								$wmfdata .= $this->_MoveTo($px, $py);
218							} else {
219								$wmfdata .= $this->_LineTo($px, $py);
220							}
221						}
222						$adjustment += $numpoints * 2;
223					}
224
225					if ($nullPen) {
226						if ($nullBrush) {
227							$op = 'n';
228						} // no op
229						else {
230							$op = 'f';
231						} // fill
232					} else {
233						if ($nullBrush) {
234							$op = 's';
235						} // stroke
236						else {
237							$op = 'b';
238						} // stroke and fill
239					}
240					if ($polyFillMode == 1 && ($op == 'b' || $op == 'f')) {
241						$op .= '*';
242					} // use even-odd fill rule
243					$wmfdata .= $op . "\n";
244					break;
245				case 0x0000:
246					$endRecord = true;
247					break;
248			}
249		}
250
251		return [1, $wmfdata, $wo, $we];
252	}
253
254	function _MoveTo($x, $y)
255	{
256		return "$x $y m\n";
257	}
258
259	// a line must have been started using _MoveTo() first
260	function _LineTo($x, $y)
261	{
262		return "$x $y l\n";
263	}
264
265	function _AddGDIObject($obj)
266	{
267		// find next available slot
268		$idx = 0;
269		if (!empty($this->gdiObjectArray)) {
270			$empty = false;
271			$i = 0;
272			while (!$empty) {
273				$empty = !isset($this->gdiObjectArray[$i]);
274				$i++;
275			}
276			$idx = $i - 1;
277		}
278		$this->gdiObjectArray[$idx] = $obj;
279	}
280
281	function _GetGDIObject($idx)
282	{
283		return $this->gdiObjectArray[$idx];
284	}
285
286	function _DeleteGDIObject($idx)
287	{
288		unset($this->gdiObjectArray[$idx]);
289	}
290}
291