1<?php
2
3namespace Mpdf;
4
5use Mpdf\Color\ColorConverter;
6
7class Gradient
8{
9	const TYPE_LINEAR = 2;
10	const TYPE_RADIAL = 3;
11
12	/**
13	 * @var \Mpdf\Mpdf
14	 */
15	private $mpdf;
16
17	/**
18	 * @var \Mpdf\SizeConverter
19	 */
20	private $sizeConverter;
21
22	/**
23	 * @var \Mpdf\Color\ColorConverter
24	 */
25	private $colorConverter;
26
27	public function __construct(Mpdf $mpdf, SizeConverter $sizeConverter, ColorConverter $colorConverter)
28	{
29		$this->mpdf = $mpdf;
30		$this->sizeConverter = $sizeConverter;
31		$this->colorConverter = $colorConverter;
32	}
33
34	// mPDF 5.3.A1
35	public function CoonsPatchMesh($x, $y, $w, $h, $patch_array = [], $x_min = 0, $x_max = 1, $y_min = 0, $y_max = 1, $colspace = 'RGB', $return = false)
36	{
37		$s = ' q ';
38		$s.=sprintf(' %.3F %.3F %.3F %.3F re W n ', $x * Mpdf::SCALE, ($this->mpdf->h - $y) * Mpdf::SCALE, $w * Mpdf::SCALE, -$h * Mpdf::SCALE);
39		$s.=sprintf(' %.3F 0 0 %.3F %.3F %.3F cm ', $w * Mpdf::SCALE, $h * Mpdf::SCALE, $x * Mpdf::SCALE, ($this->mpdf->h - ($y + $h)) * Mpdf::SCALE);
40		$n = count($this->mpdf->gradients) + 1;
41		$this->mpdf->gradients[$n]['type'] = 6; //coons patch mesh
42		$this->mpdf->gradients[$n]['colorspace'] = $colspace; //coons patch mesh
43		$bpcd = 65535; //16 BitsPerCoordinate
44		$trans = false;
45		$this->mpdf->gradients[$n]['stream'] = '';
46		for ($i = 0; $i < count($patch_array); $i++) {
47			$this->mpdf->gradients[$n]['stream'].=chr($patch_array[$i]['f']); //start with the edge flag as 8 bit
48			for ($j = 0; $j < count($patch_array[$i]['points']); $j++) {
49				//each point as 16 bit
50				if (($j % 2) == 1) { // Y coordinate (adjusted as input is From top left)
51					$patch_array[$i]['points'][$j] = (($patch_array[$i]['points'][$j] - $y_min) / ($y_max - $y_min)) * $bpcd;
52					$patch_array[$i]['points'][$j] = $bpcd - $patch_array[$i]['points'][$j];
53				} else {
54					$patch_array[$i]['points'][$j] = (($patch_array[$i]['points'][$j] - $x_min) / ($x_max - $x_min)) * $bpcd;
55				}
56				if ($patch_array[$i]['points'][$j] < 0) {
57					$patch_array[$i]['points'][$j] = 0;
58				}
59				if ($patch_array[$i]['points'][$j] > $bpcd) {
60					$patch_array[$i]['points'][$j] = $bpcd;
61				}
62				$this->mpdf->gradients[$n]['stream'].=chr(floor($patch_array[$i]['points'][$j] / 256));
63				$this->mpdf->gradients[$n]['stream'].=chr(floor($patch_array[$i]['points'][$j] % 256));
64			}
65			for ($j = 0; $j < count($patch_array[$i]['colors']); $j++) {
66				//each color component as 8 bit
67				if ($colspace === 'RGB') {
68					$this->mpdf->gradients[$n]['stream'].= $patch_array[$i]['colors'][$j][1];
69					$this->mpdf->gradients[$n]['stream'].= $patch_array[$i]['colors'][$j][2];
70					$this->mpdf->gradients[$n]['stream'].= $patch_array[$i]['colors'][$j][3];
71					if (isset($patch_array[$i]['colors'][$j][4]) && ord($patch_array[$i]['colors'][$j][4]) < 100) {
72						$trans = true;
73					}
74				} elseif ($colspace === 'CMYK') {
75					$this->mpdf->gradients[$n]['stream'].=chr(ord($patch_array[$i]['colors'][$j][1]) * 2.55);
76					$this->mpdf->gradients[$n]['stream'].=chr(ord($patch_array[$i]['colors'][$j][2]) * 2.55);
77					$this->mpdf->gradients[$n]['stream'].=chr(ord($patch_array[$i]['colors'][$j][3]) * 2.55);
78					$this->mpdf->gradients[$n]['stream'].=chr(ord($patch_array[$i]['colors'][$j][4]) * 2.55);
79					if (isset($patch_array[$i]['colors'][$j][5]) && ord($patch_array[$i]['colors'][$j][5]) < 100) {
80						$trans = true;
81					}
82				} elseif ($colspace === 'Gray') {
83					$this->mpdf->gradients[$n]['stream'].= $patch_array[$i]['colors'][$j][1];
84					if ($patch_array[$i]['colors'][$j][2] == 1) {
85						$trans = true;
86					} // transparency converted from rgba or cmyka()
87				}
88			}
89		}
90		// TRANSPARENCY
91		if ($trans) {
92			$this->mpdf->gradients[$n]['stream_trans'] = '';
93			for ($i = 0; $i < count($patch_array); $i++) {
94				$this->mpdf->gradients[$n]['stream_trans'].=chr($patch_array[$i]['f']);
95				for ($j = 0; $j < count($patch_array[$i]['points']); $j++) {
96					//each point as 16 bit
97					$this->mpdf->gradients[$n]['stream_trans'].=chr(floor($patch_array[$i]['points'][$j] / 256));
98					$this->mpdf->gradients[$n]['stream_trans'].=chr(floor($patch_array[$i]['points'][$j] % 256));
99				}
100				for ($j = 0; $j < count($patch_array[$i]['colors']); $j++) {
101					//each color component as 8 bit // OPACITY
102					if ($colspace === 'RGB') {
103						$this->mpdf->gradients[$n]['stream_trans'].=chr((int) (ord($patch_array[$i]['colors'][$j][4]) * 2.55));
104					} elseif ($colspace === 'CMYK') {
105						$this->mpdf->gradients[$n]['stream_trans'].=chr((int) (ord($patch_array[$i]['colors'][$j][5]) * 2.55));
106					} elseif ($colspace === 'Gray') {
107						$this->mpdf->gradients[$n]['stream_trans'].=chr((int) (ord($patch_array[$i]['colors'][$j][3]) * 2.55));
108					}
109				}
110			}
111			$this->mpdf->gradients[$n]['trans'] = true;
112			$s .= ' /TGS' . $n . ' gs ';
113		}
114		//paint the gradient
115		$s .= '/Sh' . $n . ' sh' . "\n";
116		//restore previous Graphic State
117		$s .= 'Q' . "\n";
118		if ($return) {
119			return $s;
120		}
121
122		$this->mpdf->_out($s);
123	}
124
125	// type = linear:2; radial: 3;
126	// Linear: $coords - array of the form (x1, y1, x2, y2) which defines the gradient vector (see linear_gradient_coords.jpg).
127	//    The default value is from left to right (x1=0, y1=0, x2=1, y2=0).
128	// Radial: $coords - array of the form (fx, fy, cx, cy, r) where (fx, fy) is the starting point of the gradient with color1,
129	//    (cx, cy) is the center of the circle with color2, and r is the radius of the circle (see radial_gradient_coords.jpg).
130	//    (fx, fy) should be inside the circle, otherwise some areas will not be defined
131	// $col = array(R,G,B/255); or array(G/255); or array(C,M,Y,K/100)
132	// $stops = array('col'=>$col [, 'opacity'=>0-1] [, 'offset'=>0-1])
133	public function Gradient($x, $y, $w, $h, $type, $stops = [], $colorspace = 'RGB', $coords = '', $extend = '', $return = false, $is_mask = false)
134	{
135		if (stripos($type, 'L') === 0) {
136			$type = self::TYPE_LINEAR;
137		} elseif (stripos($type, 'R') === 0) {
138			$type = self::TYPE_RADIAL;
139		}
140
141		if ($colorspace !== 'CMYK' && $colorspace !== 'Gray') {
142			$colorspace = 'RGB';
143		}
144		$bboxw = $w;
145		$bboxh = $h;
146		$usex = $x;
147		$usey = $y;
148		$usew = $bboxw;
149		$useh = $bboxh;
150
151		if ($type < 1) {
152			$type = self::TYPE_LINEAR;
153		}
154		if ($coords[0] !== false && preg_match('/([0-9.]+(px|em|ex|pc|pt|cm|mm|in))/i', $coords[0], $m)) {
155			$tmp = $this->sizeConverter->convert($m[1], $this->mpdf->w, $this->mpdf->FontSize, false);
156			if ($tmp) {
157				$coords[0] = $tmp / $w;
158			}
159		}
160		if ($coords[1] !== false && preg_match('/([0-9.]+(px|em|ex|pc|pt|cm|mm|in))/i', $coords[1], $m)) {
161			$tmp = $this->sizeConverter->convert($m[1], $this->mpdf->w, $this->mpdf->FontSize, false);
162			if ($tmp) {
163				$coords[1] = 1 - ($tmp / $h);
164			}
165		}
166
167		if ($type == self::TYPE_LINEAR) {
168			$angle = (isset($coords[4]) ? $coords[4] : false);
169			$repeat = (isset($coords[5]) ? $coords[5] : false);
170			// ALL POINTS SET (default for custom mPDF linear gradient) - no -moz
171			if ($coords[0] !== false && $coords[1] !== false && $coords[2] !== false && $coords[3] !== false) {
172				// do nothing - coords used as they are
173			} elseif ($angle !== false && $coords[0] !== false && $coords[1] !== false && $coords[2] === false && $coords[3] === false) {
174				// If both a <point> and <angle> are defined, the gradient axis starts from the point and runs along the angle. The end point is
175				// defined as before - in this case start points may not be in corners, and axis may not correctly fall in the right quadrant.
176				// NO end points (Angle defined & Start points)
177				if ($angle == 0 || $angle == 360) {
178					$coords[3] = $coords[1];
179					if ($coords[0] == 1) {
180						$coords[2] = 2;
181					} else {
182						$coords[2] = 1;
183					}
184				} elseif ($angle == 90) {
185					$coords[2] = $coords[0];
186					$coords[3] = 1;
187					if ($coords[1] == 1) {
188						$coords[3] = 2;
189					} else {
190						$coords[3] = 1;
191					}
192				} elseif ($angle == 180) {
193					if ($coords[4] == 0) {
194						$coords[2] = -1;
195					} else {
196						$coords[2] = 0;
197					}
198					$coords[3] = $coords[1];
199				} elseif ($angle == 270) {
200					$coords[2] = $coords[0];
201					if ($coords[1] == 0) {
202						$coords[3] = -1;
203					} else {
204						$coords[3] = 0;
205					}
206				} else {
207					$endx = 1;
208					$endy = 1;
209					if ($angle <= 90) {
210						if ($angle <= 45) {
211							$endy = tan(deg2rad($angle));
212						} else {
213							$endx = tan(deg2rad(90 - $angle));
214						}
215						$b = atan2($endy * $bboxh, $endx * $bboxw);
216						$ny = 1 - $coords[1] - (tan($b) * (1 - $coords[0]));
217						$tx = sin($b) * cos($b) * $ny;
218						$ty = cos($b) * cos($b) * $ny;
219						$coords[2] = 1 + $tx;
220						$coords[3] = 1 - $ty;
221					} elseif ($angle <= 180) {
222						if ($angle <= 135) {
223							$endx = tan(deg2rad($angle - 90));
224						} else {
225							$endy = tan(deg2rad(180 - $angle));
226						}
227						$b = atan2($endy * $bboxh, $endx * $bboxw);
228						$ny = 1 - $coords[1] - (tan($b) * $coords[0]);
229						$tx = sin($b) * cos($b) * $ny;
230						$ty = cos($b) * cos($b) * $ny;
231						$coords[2] = -$tx;
232						$coords[3] = 1 - $ty;
233					} elseif ($angle <= 270) {
234						if ($angle <= 225) {
235							$endy = tan(deg2rad($angle - 180));
236						} else {
237							$endx = tan(deg2rad(270 - $angle));
238						}
239						$b = atan2($endy * $bboxh, $endx * $bboxw);
240						$ny = $coords[1] - (tan($b) * $coords[0]);
241						$tx = sin($b) * cos($b) * $ny;
242						$ty = cos($b) * cos($b) * $ny;
243						$coords[2] = -$tx;
244						$coords[3] = $ty;
245					} else {
246						if ($angle <= 315) {
247							$endx = tan(deg2rad($angle - 270));
248						} else {
249							$endy = tan(deg2rad(360 - $angle));
250						}
251						$b = atan2($endy * $bboxh, $endx * $bboxw);
252						$ny = $coords[1] - (tan($b) * (1 - $coords[0]));
253						$tx = sin($b) * cos($b) * $ny;
254						$ty = cos($b) * cos($b) * $ny;
255						$coords[2] = 1 + $tx;
256						$coords[3] = $ty;
257					}
258				}
259			} elseif ($angle !== false && $coords[0] === false && $coords[1] === false) {
260				// -moz If the first parameter is only an <angle>, the gradient axis starts from the box's corner that would ensure the
261				// axis goes through the box. The axis runs along the specified angle. The end point of the axis is defined such that the
262				// farthest corner of the box from the starting point is perpendicular to the gradient axis at that point.
263				// NO end points or Start points (Angle defined)
264				if ($angle == 0 || $angle == 360) {
265					$coords[0] = 0;
266					$coords[1] = 0;
267					$coords[2] = 1;
268					$coords[3] = 0;
269				} elseif ($angle == 90) {
270					$coords[0] = 0;
271					$coords[1] = 0;
272					$coords[2] = 0;
273					$coords[3] = 1;
274				} elseif ($angle == 180) {
275					$coords[0] = 1;
276					$coords[1] = 0;
277					$coords[2] = 0;
278					$coords[3] = 0;
279				} elseif ($angle == 270) {
280					$coords[0] = 0;
281					$coords[1] = 1;
282					$coords[2] = 0;
283					$coords[3] = 0;
284				} else {
285					if ($angle <= 90) {
286						$coords[0] = 0;
287						$coords[1] = 0;
288						if ($angle <= 45) {
289							$endx = 1;
290							$endy = tan(deg2rad($angle));
291						} else {
292							$endx = tan(deg2rad(90 - $angle));
293							$endy = 1;
294						}
295					} elseif ($angle <= 180) {
296						$coords[0] = 1;
297						$coords[1] = 0;
298						if ($angle <= 135) {
299							$endx = tan(deg2rad($angle - 90));
300							$endy = 1;
301						} else {
302							$endx = 1;
303							$endy = tan(deg2rad(180 - $angle));
304						}
305					} elseif ($angle <= 270) {
306						$coords[0] = 1;
307						$coords[1] = 1;
308						if ($angle <= 225) {
309							$endx = 1;
310							$endy = tan(deg2rad($angle - 180));
311						} else {
312							$endx = tan(deg2rad(270 - $angle));
313							$endy = 1;
314						}
315					} else {
316						$coords[0] = 0;
317						$coords[1] = 1;
318						if ($angle <= 315) {
319							$endx = tan(deg2rad($angle - 270));
320							$endy = 1;
321						} else {
322							$endx = 1;
323							$endy = tan(deg2rad(360 - $angle));
324						}
325					}
326					$b = atan2($endy * $bboxh, $endx * $bboxw);
327					$h2 = $bboxh - ($bboxh * tan($b));
328					$px = $bboxh + ($h2 * sin($b) * cos($b));
329					$py = ($bboxh * tan($b)) + ($h2 * sin($b) * sin($b));
330					$x1 = $px / $bboxh;
331					$y1 = $py / $bboxh;
332					if ($angle <= 90) {
333						$coords[2] = $x1;
334						$coords[3] = $y1;
335					} elseif ($angle <= 180) {
336						$coords[2] = 1 - $x1;
337						$coords[3] = $y1;
338					} elseif ($angle <= 270) {
339						$coords[2] = 1 - $x1;
340						$coords[3] = 1 - $y1;
341					} else {
342						$coords[2] = $x1;
343						$coords[3] = 1 - $y1;
344					}
345				}
346			} elseif ((!isset($angle) || $angle === false) && $coords[0] !== false && $coords[1] !== false) {
347				// -moz If the first parameter to the gradient function is only a <point>, the gradient axis starts from the specified point,
348				// and ends at the point you would get if you rotated the starting point by 180 degrees about the center of the box that the
349				// gradient is to be applied to.
350				// NO angle and NO end points (Start points defined)
351				$coords[2] = 1 - $coords[0];
352				$coords[3] = 1 - $coords[1];
353				$angle = rad2deg(atan2($coords[3] - $coords[1], $coords[2] - $coords[0]));
354				if ($angle < 0) {
355					$angle += 360;
356				} elseif ($angle > 360) {
357					$angle -= 360;
358				}
359				if ($angle != 0 && $angle != 360 && $angle != 90 && $angle != 180 && $angle != 270) {
360					if ($w >= $h) {
361						$coords[1] *= $h / $w;
362						$coords[3] *= $h / $w;
363						$usew = $useh = $bboxw;
364						$usey -= ($w - $h);
365					} else {
366						$coords[0] *= $w / $h;
367						$coords[2] *= $w / $h;
368						$usew = $useh = $bboxh;
369					}
370				}
371			} else {
372				// default values T2B
373				// -moz If neither a <point> or <angle> is specified, i.e. the entire function consists of only <stop> values, the gradient
374				// axis starts from the top of the box and runs vertically downwards, ending at the bottom of the box.
375				// All values are set in parseMozGradient - so won't appear here
376				$coords = [0, 0, 1, 0]; // default for original linear gradient (L2R)
377			}
378		} elseif ($type == self::TYPE_RADIAL) {
379			$radius = (isset($coords[4]) ? $coords[4] : false);
380			$shape = (isset($coords[6]) ? $coords[6] : false);
381			$size = (isset($coords[7]) ? $coords[7] : false);
382			$repeat = (isset($coords[8]) ? $coords[8] : false);
383			// ALL POINTS AND RADIUS SET (default for custom mPDF radial gradient) - no -moz
384			if ($coords[0] !== false && $coords[1] !== false && $coords[2] !== false && $coords[3] !== false && $coords[4] !== false) {
385				// If a <point> is defined
386				// do nothing - coords used as they are
387			} elseif ($shape !== false && $size !== false) {
388				if ($coords[2] == false) {
389					$coords[2] = $coords[0];
390				}
391				if ($coords[3] == false) {
392					$coords[3] = $coords[1];
393				}
394				// ELLIPSE
395				if ($shape === 'ellipse') {
396					$corner1 = sqrt(($coords[0] ** 2) + ($coords[1] ** 2));
397					$corner2 = sqrt(($coords[0] ** 2) + ((1 - $coords[1]) ** 2));
398					$corner3 = sqrt(((1 - $coords[0]) ** 2) + ($coords[1] ** 2));
399					$corner4 = sqrt(((1 - $coords[0]) ** 2) + ((1 - $coords[1]) ** 2));
400					if ($size === 'closest-side') {
401						$radius = min($coords[0], $coords[1], 1 - $coords[0], 1 - $coords[1]);
402					} elseif ($size === 'closest-corner') {
403						$radius = min($corner1, $corner2, $corner3, $corner4);
404					} elseif ($size === 'farthest-side') {
405						$radius = max($coords[0], $coords[1], 1 - $coords[0], 1 - $coords[1]);
406					} else {
407						$radius = max($corner1, $corner2, $corner3, $corner4);
408					} // farthest corner (default)
409				} elseif ($shape === 'circle') {
410					if ($w >= $h) {
411						$coords[1] = $coords[3] = ($coords[1] * $h / $w);
412						$corner1 = sqrt(($coords[0] ** 2) + ($coords[1] ** 2));
413						$corner2 = sqrt(($coords[0] ** 2) + ((($h / $w) - $coords[1]) ** 2));
414						$corner3 = sqrt(((1 - $coords[0]) ** 2) + ($coords[1] ** 2));
415						$corner4 = sqrt(((1 - $coords[0]) ** 2) + ((($h / $w) - $coords[1]) ** 2));
416						if ($size === 'closest-side') {
417							$radius = min($coords[0], $coords[1], 1 - $coords[0], ($h / $w) - $coords[1]);
418						} elseif ($size === 'closest-corner') {
419							$radius = min($corner1, $corner2, $corner3, $corner4);
420						} elseif ($size === 'farthest-side') {
421							$radius = max($coords[0], $coords[1], 1 - $coords[0], ($h / $w) - $coords[1]);
422						} elseif ($size === 'farthest-corner') {
423							$radius = max($corner1, $corner2, $corner3, $corner4);
424						} // farthest corner (default)
425						$usew = $useh = $bboxw;
426						$usey -= ($w - $h);
427					} else {
428						$coords[0] = $coords[2] = ($coords[0] * $w / $h);
429						$corner1 = sqrt(($coords[0] ** 2) + ($coords[1] ** 2));
430						$corner2 = sqrt(($coords[0] ** 2) + ((1 - $coords[1]) ** 2));
431						$corner3 = sqrt(((($w / $h) - $coords[0]) ** 2) + ($coords[1] ** 2));
432						$corner4 = sqrt(((($w / $h) - $coords[0]) ** 2) + ((1 - $coords[1]) ** 2));
433						if ($size === 'closest-side') {
434							$radius = min($coords[0], $coords[1], ($w / $h) - $coords[0], 1 - $coords[1]);
435						} elseif ($size === 'closest-corner') {
436							$radius = min($corner1, $corner2, $corner3, $corner4);
437						} elseif ($size === 'farthest-side') {
438							$radius = max($coords[0], $coords[1], ($w / $h) - $coords[0], 1 - $coords[1]);
439						} elseif ($size === 'farthest-corner') {
440							$radius = max($corner1, $corner2, $corner3, $corner4);
441						} // farthest corner (default)
442						$usew = $useh = $bboxh;
443					}
444				}
445				if ($radius == 0) {
446					$radius = 0.001;
447				} // to prevent error
448				$coords[4] = $radius;
449			} else {
450				// -moz If entire function consists of only <stop> values
451				// All values are set in parseMozGradient - so won't appear here
452				$coords = [0.5, 0.5, 0.5, 0.5]; // default for radial gradient (centred)
453			}
454		}
455		$s = ' q';
456		$s .= sprintf(' %.3F %.3F %.3F %.3F re W n', $x * Mpdf::SCALE, ($this->mpdf->h - $y) * Mpdf::SCALE, $w * Mpdf::SCALE, -$h * Mpdf::SCALE) . "\n";
457		$s .= sprintf(' %.3F 0 0 %.3F %.3F %.3F cm', $usew * Mpdf::SCALE, $useh * Mpdf::SCALE, $usex * Mpdf::SCALE, ($this->mpdf->h - ($usey + $useh)) * Mpdf::SCALE) . "\n";
458
459		$n = count($this->mpdf->gradients) + 1;
460		$this->mpdf->gradients[$n]['type'] = $type;
461		$this->mpdf->gradients[$n]['colorspace'] = $colorspace;
462		$trans = false;
463		$this->mpdf->gradients[$n]['is_mask'] = $is_mask;
464		if ($is_mask) {
465			$trans = true;
466		}
467		if (count($stops) == 1) {
468			$stops[1] = $stops[0];
469		}
470		if (!isset($stops[0]['offset'])) {
471			$stops[0]['offset'] = 0;
472		}
473		if (!isset($stops[count($stops) - 1]['offset'])) {
474			$stops[count($stops) - 1]['offset'] = 1;
475		}
476
477		// Fix stop-offsets set as absolute lengths
478		if ($type == self::TYPE_LINEAR) {
479			$axisx = ($coords[2] - $coords[0]) * $usew;
480			$axisy = ($coords[3] - $coords[1]) * $useh;
481			$axis_length = sqrt(($axisx ** 2) + ($axisy ** 2));
482		} else {
483			$axis_length = $coords[4] * $usew;
484		} // Absolute lengths are meaningless for an ellipse - Firefox uses Width as reference
485
486		for ($i = 0; $i < count($stops); $i++) {
487			if (isset($stops[$i]['offset']) && preg_match('/([0-9.]+(px|em|ex|pc|pt|cm|mm|in))/i', $stops[$i]['offset'], $m)) {
488				$tmp = $this->sizeConverter->convert($m[1], $this->mpdf->w, $this->mpdf->FontSize, false);
489				$stops[$i]['offset'] = $tmp / $axis_length;
490			}
491		}
492
493
494		if (isset($stops[0]['offset']) && $stops[0]['offset'] > 0) {
495			$firststop = $stops[0];
496			$firststop['offset'] = 0;
497			array_unshift($stops, $firststop);
498		}
499		if (!$repeat && isset($stops[count($stops) - 1]['offset']) && $stops[count($stops) - 1]['offset'] < 1) {
500			$endstop = $stops[count($stops) - 1];
501			$endstop['offset'] = 1;
502			$stops[] = $endstop;
503		}
504		if ($stops[0]['offset'] > $stops[count($stops) - 1]['offset']) {
505			$stops[0]['offset'] = 0;
506			$stops[count($stops) - 1]['offset'] = 1;
507		}
508
509		for ($i = 0; $i < count($stops); $i++) {
510			// mPDF 5.3.74
511			if ($colorspace === 'CMYK') {
512				$this->mpdf->gradients[$n]['stops'][$i]['col'] = sprintf('%.3F %.3F %.3F %.3F', ord($stops[$i]['col']{1}) / 100, ord($stops[$i]['col']{2}) / 100, ord($stops[$i]['col']{3}) / 100, ord($stops[$i]['col']{4}) / 100);
513			} elseif ($colorspace === 'Gray') {
514				$this->mpdf->gradients[$n]['stops'][$i]['col'] = sprintf('%.3F', ord($stops[$i]['col']{1}) / 255);
515			} else {
516				$this->mpdf->gradients[$n]['stops'][$i]['col'] = sprintf('%.3F %.3F %.3F', ord($stops[$i]['col']{1}) / 255, ord($stops[$i]['col']{2}) / 255, ord($stops[$i]['col']{3}) / 255);
517			}
518			if (!isset($stops[$i]['opacity'])) {
519				$stops[$i]['opacity'] = 1;
520			} elseif ($stops[$i]['opacity'] > 1 || $stops[$i]['opacity'] < 0) {
521				$stops[$i]['opacity'] = 1;
522			} elseif ($stops[$i]['opacity'] < 1) {
523				$trans = true;
524			}
525			$this->mpdf->gradients[$n]['stops'][$i]['opacity'] = $stops[$i]['opacity'];
526			// OFFSET
527			if ($i > 0 && $i < (count($stops) - 1)) {
528				if (!isset($stops[$i]['offset']) || (isset($stops[$i + 1]['offset']) && $stops[$i]['offset'] > $stops[$i + 1]['offset']) || $stops[$i]['offset'] < $stops[$i - 1]['offset']) {
529					if (isset($stops[$i - 1]['offset']) && isset($stops[$i + 1]['offset'])) {
530						$stops[$i]['offset'] = ($stops[$i - 1]['offset'] + $stops[$i + 1]['offset']) / 2;
531					} else {
532						for ($j = ($i + 1); $j < count($stops); $j++) {
533							if (isset($stops[$j]['offset'])) {
534								break;
535							}
536						}
537						$int = ($stops[$j]['offset'] - $stops[$i - 1]['offset']) / ($j - $i + 1);
538						for ($f = 0; $f < ($j - $i - 1); $f++) {
539							$stops[$i + $f]['offset'] = $stops[$i + $f - 1]['offset'] + $int;
540						}
541					}
542				}
543			}
544			$this->mpdf->gradients[$n]['stops'][$i]['offset'] = $stops[$i]['offset'];
545		}
546
547		if ($repeat) {
548			$ns = count($this->mpdf->gradients[$n]['stops']);
549			$offs = [];
550			for ($i = 0; $i < $ns; $i++) {
551				$offs[$i] = $this->mpdf->gradients[$n]['stops'][$i]['offset'];
552			}
553			$gp = 0;
554			$inside = true;
555			while ($inside) {
556				$gp++;
557				for ($i = 0; $i < $ns; $i++) {
558					$this->mpdf->gradients[$n]['stops'][($ns * $gp) + $i] = $this->mpdf->gradients[$n]['stops'][($ns * ($gp - 1)) + $i];
559					$tmp = $this->mpdf->gradients[$n]['stops'][($ns * ($gp - 1)) + ($ns - 1)]['offset'] + $offs[$i];
560					if ($tmp < 1) {
561						$this->mpdf->gradients[$n]['stops'][($ns * $gp) + $i]['offset'] = $tmp;
562					} else {
563						$this->mpdf->gradients[$n]['stops'][($ns * $gp) + $i]['offset'] = 1;
564						$inside = false;
565						break;
566					}
567				}
568			}
569		}
570
571		if ($trans) {
572			$this->mpdf->gradients[$n]['trans'] = true;
573			$s .= ' /TGS' . $n . ' gs ';
574		}
575		if (!is_array($extend) || count($extend) < 1) {
576			$extend = ['true', 'true']; // These are supposed to be quoted - appear in PDF file as text
577		}
578		$this->mpdf->gradients[$n]['coords'] = $coords;
579		$this->mpdf->gradients[$n]['extend'] = $extend;
580		//paint the gradient
581		$s .= '/Sh' . $n . ' sh ' . "\n";
582		//restore previous Graphic State
583		$s .= ' Q ' . "\n";
584		if ($return) {
585			return $s;
586		}
587
588		$this->mpdf->_out($s);
589	}
590
591	private function parseMozLinearGradient($m, $repeat)
592	{
593		$g = [];
594		$g['type'] = self::TYPE_LINEAR;
595		$g['colorspace'] = 'RGB';
596		$g['extend'] = ['true', 'true'];
597		$v = trim($m[1]);
598		// Change commas inside e.g. rgb(x,x,x)
599		while (preg_match('/(\([^\)]*?),/', $v)) {
600			$v = preg_replace('/(\([^\)]*?),/', '\\1@', $v);
601		}
602		// Remove spaces inside e.g. rgb(x, x, x)
603		while (preg_match('/(\([^\)]*?)[ ]/', $v)) {
604			$v = preg_replace('/(\([^\)]*?)[ ]/', '\\1', $v);
605		}
606		$bgr = preg_split('/\s*,\s*/', $v);
607		for ($i = 0; $i < count($bgr); $i++) {
608			$bgr[$i] = preg_replace('/@/', ',', $bgr[$i]);
609		}
610		// Is first part $bgr[0] a valid point/angle?
611		$first = preg_split('/\s+/', trim($bgr[0]));
612		if (preg_match('/(left|center|right|bottom|top|deg|grad|rad)/i', $bgr[0]) && !preg_match('/(<#|rgb|rgba|hsl|hsla)/i', $bgr[0])) {
613			$startStops = 1;
614		} elseif (trim($first[count($first) - 1]) === '0') {
615			$startStops = 1;
616		} else {
617			$check = $this->colorConverter->convert($first[0], $this->mpdf->PDFAXwarnings);
618			$startStops = 1;
619			if ($check) {
620				$startStops = 0;
621			}
622		}
623		// first part a valid point/angle?
624		if ($startStops === 1) { // default values
625			// [<point> || <angle>,] = [<% em px left center right bottom top> || <deg grad rad 0>,]
626			if (preg_match('/([\-]*[0-9\.]+)(deg|grad|rad)/i', $bgr[0], $m)) {
627				$angle = $m[1] + 0;
628				if (strtolower($m[2]) === 'grad') {
629					$angle *= (360 / 400);
630				} elseif (strtolower($m[2]) === 'rad') {
631					$angle = rad2deg($angle);
632				}
633				while ($angle < 0) {
634					$angle += 360;
635				}
636				$angle %= 360;
637			} elseif (trim($first[count($first) - 1]) === '0') {
638				$angle = 0;
639			}
640			if (stripos($bgr[0], 'left') !== false) {
641				$startx = 0;
642			} elseif (stripos($bgr[0], 'right') !== false) {
643				$startx = 1;
644			}
645			if (stripos($bgr[0], 'top') !== false) {
646				$starty = 1;
647			} elseif (stripos($bgr[0], 'bottom') !== false) {
648				$starty = 0;
649			}
650			// Check for %? ?% or %%
651			if (preg_match('/(\d+)[%]/i', $first[0], $m)) {
652				$startx = $m[1] / 100;
653			} elseif (!isset($startx) && preg_match('/([0-9.]+(px|em|ex|pc|pt|cm|mm|in))/i', $first[0], $m)) {
654				$tmp = $this->sizeConverter->convert($m[1], $this->mpdf->w, $this->mpdf->FontSize, false);
655				if ($tmp) {
656					$startx = $m[1];
657				}
658			}
659			if (isset($first[1]) && preg_match('/(\d+)[%]/i', $first[1], $m)) {
660				$starty = 1 - ($m[1] / 100);
661			} elseif (!isset($starty) && isset($first[1]) && preg_match('/([0-9.]+(px|em|ex|pc|pt|cm|mm|in))/i', $first[1], $m)) {
662				$tmp = $this->sizeConverter->convert($m[1], $this->mpdf->w, $this->mpdf->FontSize, false);
663				if ($tmp) {
664					$starty = $m[1];
665				}
666			}
667			if (isset($startx) && !isset($starty)) {
668				$starty = 0.5;
669			}
670			if (!isset($startx) && isset($starty)) {
671				$startx = 0.5;
672			}
673		} else {
674			// If neither a <point> or <angle> is specified, i.e. the entire function consists of only <stop> values,
675			// the gradient axis starts from the top of the box and runs vertically downwards, ending at the bottom of
676			// the box.
677			$starty = 1;
678			$startx = 0.5;
679			$endy = 0;
680			$endx = 0.5;
681		}
682		if (!isset($startx)) {
683			$startx = false;
684		}
685		if (!isset($starty)) {
686			$starty = false;
687		}
688		if (!isset($endx)) {
689			$endx = false;
690		}
691		if (!isset($endy)) {
692			$endy = false;
693		}
694		if (!isset($angle)) {
695			$angle = false;
696		}
697		$g['coords'] = [$startx, $starty, $endx, $endy, $angle, $repeat];
698		$g['stops'] = [];
699		for ($i = $startStops; $i < count($bgr); $i++) {
700			// parse stops
701			$el = preg_split('/\s+/', trim($bgr[$i]));
702			// mPDF 5.3.74
703			$col = $this->colorConverter->convert($el[0], $this->mpdf->PDFAXwarnings);
704			if (!$col) {
705				$col = $this->colorConverter->convert(255, $this->mpdf->PDFAXwarnings);
706			}
707			if ($col{0} == 1) {
708				$g['colorspace'] = 'Gray';
709			} elseif ($col{0} == 4 || $col{0} == 6) {
710				$g['colorspace'] = 'CMYK';
711			}
712
713			$g['stops'][] = $this->getStop($col, $el, true);
714		}
715		return $g;
716	}
717
718	private function parseMozRadialGradient($m, $repeat)
719	{
720		$g = [];
721		$g['type'] = self::TYPE_RADIAL;
722		$g['colorspace'] = 'RGB';
723		$g['extend'] = ['true', 'true'];
724		$v = trim($m[1]);
725		// Change commas inside e.g. rgb(x,x,x)
726		while (preg_match('/(\([^\)]*?),/', $v)) {
727			$v = preg_replace('/(\([^\)]*?),/', '\\1@', $v);
728		}
729		// Remove spaces inside e.g. rgb(x, x, x)
730		while (preg_match('/(\([^\)]*?)[ ]/', $v)) {
731			$v = preg_replace('/(\([^\)]*?)[ ]/', '\\1', $v);
732		}
733		$bgr = preg_split('/\s*,\s*/', $v);
734		for ($i = 0; $i < count($bgr); $i++) {
735			$bgr[$i] = preg_replace('/@/', ',', $bgr[$i]);
736		}
737
738		// Is first part $bgr[0] a valid point/angle?
739		$startStops = 0;
740		$pos_angle = false;
741		$shape_size = false;
742		$first = preg_split('/\s+/', trim($bgr[0]));
743		$checkCol = $this->colorConverter->convert($first[0], $this->mpdf->PDFAXwarnings);
744		if (preg_match('/(left|center|right|bottom|top|deg|grad|rad)/i', $bgr[0]) && !preg_match('/(<#|rgb|rgba|hsl|hsla)/i', $bgr[0])) {
745			$startStops = 1;
746			$pos_angle = $bgr[0];
747		} elseif (trim($first[count($first) - 1]) === '0') {
748			$startStops = 1;
749			$pos_angle = $bgr[0];
750		} elseif (preg_match('/(circle|ellipse|closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)/i', $bgr[0])) {
751			$startStops = 1;
752			$shape_size = $bgr[0];
753		} elseif (!$checkCol) {
754			$startStops = 1;
755			$pos_angle = $bgr[0];
756		}
757		if (preg_match('/(circle|ellipse|closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)/i', $bgr[1])) {
758			$startStops = 2;
759			$shape_size = $bgr[1];
760		}
761
762		// If valid point/angle?
763		if ($pos_angle) { // default values
764			// [<point> || <angle>,] = [<% em px left center right bottom top> || <deg grad rad 0>,]
765			if (stripos($pos_angle, 'left') !== false) {
766				$startx = 0;
767			} elseif (stripos($pos_angle, 'right') !== false) {
768				$startx = 1;
769			}
770			if (stripos($pos_angle, 'top') !== false) {
771				$starty = 1;
772			} elseif (stripos($pos_angle, 'bottom') !== false) {
773				$starty = 0;
774			}
775			// Check for %? ?% or %%
776			if (preg_match('/(\d+)[%]/i', $first[0], $m)) {
777				$startx = $m[1] / 100;
778			} elseif (!isset($startx) && preg_match('/([0-9.]+(px|em|ex|pc|pt|cm|mm|in))/i', $first[0], $m)) {
779				$tmp = $this->sizeConverter->convert($m[1], $this->mpdf->w, $this->mpdf->FontSize, false);
780				if ($tmp) {
781					$startx = $m[1];
782				}
783			}
784			if (isset($first[1]) && preg_match('/(\d+)[%]/i', $first[1], $m)) {
785				$starty = 1 - ($m[1] / 100);
786			} elseif (!isset($starty) && isset($first[1]) && preg_match('/([0-9.]+(px|em|ex|pc|pt|cm|mm|in))/i', $first[1], $m)) {
787				$tmp = $this->sizeConverter->convert($m[1], $this->mpdf->w, $this->mpdf->FontSize, false);
788				if ($tmp) {
789					$starty = $m[1];
790				}
791			}
792
793			if (!isset($starty)) {
794				$starty = 0.5;
795			}
796			if (!isset($startx)) {
797				$startx = 0.5;
798			}
799		} else {
800			// If neither a <point> or <angle> is specified, i.e. the entire function consists of only <stop> values,
801			// the gradient axis starts from the top of the box and runs vertically downwards, ending at the bottom of
802			// the box. default values Center
803			$starty = 0.5;
804			$startx = 0.5;
805			$endy = 0.5;
806			$endx = 0.5;
807		}
808
809		// If valid shape/size?
810		$shape = 'ellipse'; // default
811		$size = 'farthest-corner'; // default
812		if ($shape_size) { // default values
813			if (preg_match('/(circle|ellipse)/i', $shape_size, $m)) {
814				$shape = $m[1];
815			}
816			if (preg_match('/(closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)/i', $shape_size, $m)) {
817				$size = $m[1];
818				if ($size === 'contain') {
819					$size = 'closest-side';
820				} elseif ($size === 'cover') {
821					$size = 'farthest-corner';
822				}
823			}
824		}
825
826		if (!isset($startx)) {
827			$startx = false;
828		}
829		if (!isset($starty)) {
830			$starty = false;
831		}
832		if (!isset($endx)) {
833			$endx = false;
834		}
835		if (!isset($endy)) {
836			$endy = false;
837		}
838		$radius = false;
839		$angle = 0;
840		$g['coords'] = [$startx, $starty, $endx, $endy, $radius, $angle, $shape, $size, $repeat];
841
842		$g['stops'] = [];
843		for ($i = $startStops; $i < count($bgr); $i++) {
844			// parse stops
845			$el = preg_split('/\s+/', trim($bgr[$i]));
846			// mPDF 5.3.74
847			$col = $this->colorConverter->convert($el[0], $this->mpdf->PDFAXwarnings);
848			if (!$col) {
849				$col = $this->colorConverter->convert(255, $this->mpdf->PDFAXwarnings);
850			}
851			if ($col{0} == 1) {
852				$g['colorspace'] = 'Gray';
853			} elseif ($col{0} == 4 || $col{0} == 6) {
854				$g['colorspace'] = 'CMYK';
855			}
856			$g['stops'][] = $this->getStop($col, $el);
857		}
858		return $g;
859	}
860
861	private function getStop($col, $el, $convertOffset = false)
862	{
863		$stop = [
864			'col' => $col,
865		];
866
867		if ($col{0} == 5) {
868			// transparency from rgba()
869			$stop['opacity'] = ord($col{4}) / 100;
870		} elseif ($col{0} == 6) {
871			// transparency from cmyka()
872			$stop['opacity'] = ord($col{5}) / 100;
873		} elseif ($col{0} == 1 && $col{2} == 1) {
874			// transparency converted from rgba or cmyka()
875			$stop['opacity'] = ord($col{3}) / 100;
876		}
877
878		if (isset($el[1])) {
879			if (preg_match('/(\d+)[%]/', $el[1], $m)) {
880				$stop['offset'] = $m[1] / 100;
881				if ($stop['offset'] > 1) {
882					unset($stop['offset']);
883				}
884			} elseif (preg_match('/([0-9.]+(px|em|ex|pc|pt|cm|mm|in))/i', $el[1], $m)) {
885				if ($convertOffset) {
886					$tmp = $this->sizeConverter->convert($m[1], $this->mpdf->w, $this->mpdf->FontSize, false);
887					if ($tmp) {
888						$stop['offset'] = $m[1];
889					}
890				} else {
891					$stop['offset'] = $el[1];
892				}
893			}
894		}
895
896		return $stop;
897	}
898
899	public function parseMozGradient($bg)
900	{
901		//	background[-image]: -moz-linear-gradient(left, #c7Fdde 20%, #FF0000 );
902		//	background[-image]: linear-gradient(left, #c7Fdde 20%, #FF0000 ); // CSS3
903		$repeat = strpos($bg, 'repeating-') !== false;
904
905		if (preg_match('/linear-gradient\((.*)\)/', $bg, $m)) {
906			$g = $this->parseMozLinearGradient($m, $repeat);
907			if (count($g['stops'])) {
908				return $g;
909			}
910		} elseif (preg_match('/radial-gradient\((.*)\)/', $bg, $m)) {
911			$g = $this->parseMozRadialGradient($m, $repeat);
912			if (count($g['stops'])) {
913				return $g;
914			}
915		}
916		return [];
917	}
918
919	public function parseBackgroundGradient($bg)
920	{
921		// background-gradient: linear #00FFFF #FFFF00 0 0.5 1 0.5;  or
922		// background-gradient: radial #00FFFF #FFFF00 0.5 0.5 1 1 1.2;
923
924		$v = trim($bg);
925		$bgr = preg_split('/\s+/', $v);
926		$count_bgr = count($bgr);
927		$g = [];
928		if ($count_bgr > 6) {
929			if (stripos($bgr[0], 'L') === 0 && $count_bgr === 7) {  // linear
930				$g['type'] = self::TYPE_LINEAR;
931				//$coords = array(0,0,1,1 );	// 0 0 1 0 or 0 1 1 1 is L 2 R; 1,1,0,1 is R2L; 1,1,1,0 is T2B; 1,0,1,1 is B2T
932				// Linear: $coords - array of the form (x1, y1, x2, y2) which defines the gradient vector (see linear_gradient_coords.jpg).
933				//    The default value is from left to right (x1=0, y1=0, x2=1, y2=0).
934				$g['coords'] = [$bgr[3], $bgr[4], $bgr[5], $bgr[6]];
935			} elseif ($count_bgr === 8) { // radial
936				$g['type'] = self::TYPE_RADIAL;
937				// Radial: $coords - array of the form (fx, fy, cx, cy, r) where (fx, fy) is the starting point of the gradient with color1,
938				//    (cx, cy) is the center of the circle with color2, and r is the radius of the circle (see radial_gradient_coords.jpg).
939				//    (fx, fy) should be inside the circle, otherwise some areas will not be defined
940				$g['coords'] = [$bgr[3], $bgr[4], $bgr[5], $bgr[6], $bgr[7]];
941			}
942			$g['colorspace'] = 'RGB';
943			// mPDF 5.3.74
944			$cor = $this->colorConverter->convert($bgr[1], $this->mpdf->PDFAXwarnings);
945			if ($cor{0} == 1) {
946				$g['colorspace'] = 'Gray';
947			} elseif ($cor{0} == 4 || $cor{0} == 6) {
948				$g['colorspace'] = 'CMYK';
949			}
950			if ($cor) {
951				$g['col'] = $cor;
952			} else {
953				$g['col'] = $this->colorConverter->convert(255, $this->mpdf->PDFAXwarnings);
954			}
955			$cor = $this->colorConverter->convert($bgr[2], $this->mpdf->PDFAXwarnings);
956			if ($cor) {
957				$g['col2'] = $cor;
958			} else {
959				$g['col2'] = $this->colorConverter->convert(255, $this->mpdf->PDFAXwarnings);
960			}
961			$g['extend'] = ['true', 'true'];
962			$g['stops'] = [['col' => $g['col'], 'opacity' => 1, 'offset' => 0], ['col' => $g['col2'], 'opacity' => 1, 'offset' => 1]];
963			return $g;
964		}
965		return false;
966	}
967}
968