1<?php
2
3require_once dirname(__FILE__).'/ICanvas.php';
4
5class GDCanvas implements ICanvas {
6	public function __construct($xSize, $ySize, $transparent=true) {
7		$this->picture = imagecreatetruecolor($xSize, $ySize);
8
9		$C_White = $this->allocateColor(new Color(255, 255, 255));
10		imagefilledrectangle($this->picture, 0, 0, $xSize, $ySize, $C_White);
11		if($transparent) imagecolortransparent($this->picture, $C_White);
12
13		$this->antialiasQuality = 0;
14	}
15
16	function drawFilledRectangle(Point $corner1, Point $corner2, Color $color,
17								 ShadowProperties $shadowProperties, $drawBorder = false,
18								 $alpha = 100, $lineWidth = 1, $lineDotSize = 0) {
19		if ($corner2->getX() < $corner1->getX()) {
20			$newCorner1 = new Point($corner2->getX(), $corner1->getY());
21			$newCorner2 = new Point($corner1->getX(), $corner2->getY());
22
23			$corner1 = $newCorner1;
24			$corner2 = $newCorner2;
25		}
26
27		if ($corner2->getY() < $corner1->getY()) {
28			$newCorner1 = new Point($corner1->getX(), $corner2->getY());
29			$newCorner2 = new Point($corner2->getX(), $corner1->getY());
30
31			$corner1 = $newCorner1;
32			$corner2 = $newCorner2;
33		}
34
35		$X1 = $corner1->getX();
36		$Y1 = $corner1->getY();
37
38		$X2 = $corner2->getX();
39		$Y2 = $corner2->getY();
40
41		if ($alpha == 100) {
42			/* Process shadows */
43			if ($shadowProperties->active) {
44				$this->drawFilledRectangle(new Point($X1 + $shadowProperties->xDistance,
45													 $Y1 + $shadowProperties->yDistance),
46										   new Point($X2 + $shadowProperties->xDistance,
47													 $Y2 + $shadowProperties->yDistance),
48										   $shadowProperties->color,
49										   ShadowProperties::NoShadow(),
50										   FALSE,
51										   $shadowProperties->alpha);
52				if ($shadowProperties->blur != 0) {
53					$AlphaDecay = ($shadowProperties->alpha / $shadowProperties->blur);
54
55					for($i = 1; $i <= $shadowProperties->blur; $i ++)
56						$this->drawFilledRectangle(new Point($X1 + $shadowProperties->xDistance - $i / 2,
57															 $Y1 + $shadowProperties->yDistance - $i / 2),
58												   new Point($X2 + $shadowProperties->xDistance - $i / 2,
59															 $Y2 + $shadowProperties->yDistance - $i / 2),
60												   $shadowProperties->color,
61												   ShadowProperties::NoShadow(),
62												   FALSE,
63												   $shadowProperties->alpha - $AlphaDecay * $i);
64					for($i = 1; $i <= $shadowProperties->blur; $i ++)
65						$this->drawFilledRectangle(new Point($X1 + $shadowProperties->xDistance + $i / 2,
66															 $Y1 + $shadowProperties->yDistance + $i / 2),
67												   new Point($X2 + $shadowProperties->xDistance + $i / 2,
68															 $Y2 + $shadowProperties->xDistance + $i / 2),
69												   $shadowProperties->color,
70												   ShadowProperties::NoShadow(),
71												   FALSE,
72												   $shadowProperties->alpha - $AlphaDecay * $i);
73				}
74			}
75
76			$C_Rectangle = $this->allocateColor($color);
77			imagefilledrectangle($this->picture, round ( $X1 ), round ( $Y1 ), round ( $X2 ), round ( $Y2 ), $C_Rectangle );
78		} else {
79			$LayerWidth = abs ( $X2 - $X1 ) + 2;
80			$LayerHeight = abs ( $Y2 - $Y1 ) + 2;
81
82			$this->Layers [0] = imagecreatetruecolor ( $LayerWidth, $LayerHeight );
83			$C_White = imagecolorallocate( $this->Layers [0], 255, 255, 255);
84			imagefilledrectangle ( $this->Layers [0], 0, 0, $LayerWidth, $LayerHeight, $C_White );
85			imagecolortransparent ( $this->Layers [0], $C_White );
86
87			$C_Rectangle = imagecolorallocate( $this->Layers [0], $color->r, $color->g, $color->b);
88			imagefilledrectangle ( $this->Layers [0], round ( 1 ), round ( 1 ), round ( $LayerWidth - 1 ), round ( $LayerHeight - 1 ), $C_Rectangle );
89
90			imagecopymerge ($this->picture, $this->Layers [0], round ( min ( $X1, $X2 ) - 1 ), round ( min ( $Y1, $Y2 ) - 1 ), 0, 0, $LayerWidth, $LayerHeight, $alpha);
91			imagedestroy ( $this->Layers [0] );
92		}
93
94		if ($drawBorder) {
95			$this->drawRectangle(new Point($X1, $Y1),
96								 new Point($X2, $Y2),
97								 $color,
98								 $lineWidth,
99								 $lineDotSize,
100								 ShadowProperties::NoShadow());
101		}
102	}
103
104	public function drawRectangle(Point $corner1, Point $corner2, Color $color, $lineWidth, $lineDotSize, ShadowProperties $shadowProperties) {
105		$X1 = $corner1->getX() - .2;
106		$Y1 = $corner1->getY() - .2;
107		$X2 = $corner2->getX() + .2;
108		$Y2 = $corner2->getY() + .2;
109		$this->drawLine(new Point($X1, $Y1),
110						new Point($X2, $Y1),
111						$color,
112						$lineWidth,
113						$lineDotSize,
114						$shadowProperties);
115
116		$this->drawLine(new Point($X2, $Y1),
117						new Point($X2, $Y2),
118						$color,
119						$lineWidth,
120						$lineDotSize,
121						$shadowProperties);
122
123		$this->drawLine(new Point($X2, $Y2),
124						new Point($X1, $Y2),
125						$color,
126						$lineWidth,
127						$lineDotSize,
128						$shadowProperties);
129
130		$this->drawLine(new Point($X1, $Y2),
131						new Point($X1, $Y1),
132						$color,
133						$lineWidth,
134						$lineDotSize,
135						$shadowProperties);
136
137	}
138
139	public function drawRoundedRectangle(Point $point1, Point $point2, $radius, Color $color, $lineWidth, $lineDotSize, ShadowProperties $shadowProperties) {
140		$Step = 90 / ((M_PI * $radius) / 2);
141
142		for($i = 0; $i <= 90; $i = $i + $Step) {
143			$X = cos ( ($i + 180) * M_PI / 180 ) * $radius + $point1->getX() + $radius;
144			$Y = sin ( ($i + 180) * M_PI / 180 ) * $radius + $point1->getY() + $radius;
145			$this->drawAntialiasPixel(new Point($X, $Y), $color, $shadowProperties);
146
147			$X = cos ( ($i - 90) * M_PI / 180 ) * $radius + $point2->getX() - $radius;
148			$Y = sin ( ($i - 90) * M_PI / 180 ) * $radius + $point1->getY() + $radius;
149			$this->drawAntialiasPixel(new Point($X, $Y), $color, $shadowProperties);
150
151			$X = cos ( ($i) * M_PI / 180 ) * $radius + $point2->getX() - $radius;
152			$Y = sin ( ($i) * M_PI / 180 ) * $radius + $point2->getY() - $radius;
153			$this->drawAntialiasPixel(new Point($X, $Y), $color, $shadowProperties);
154
155			$X = cos ( ($i + 90) * M_PI / 180 ) * $radius + $point1->getX() + $radius;
156			$Y = sin ( ($i + 90) * M_PI / 180 ) * $radius + $point2->getY() - $radius;
157			$this->drawAntialiasPixel(new Point($X, $Y), $color, $shadowProperties);
158		}
159
160		$X1 = $point1->getX() - .2;
161		$Y1 = $point1->getY() - .2;
162		$X2 = $point2->getX() + .2;
163		$Y2 = $point2->getY() + .2;
164		$this->drawLine(new Point($X1 + $radius, $Y1),
165						new Point($X2 - $radius, $Y1),
166						$color,
167						$lineWidth,
168						$lineDotSize,
169						$shadowProperties);
170
171		$this->drawLine(new Point($X2, $Y1 + $radius),
172						new Point($X2, $Y2 - $radius),
173						$color,
174						$lineWidth,
175						$lineDotSize,
176						$shadowProperties);
177
178		$this->drawLine(new Point($X2 - $radius, $Y2),
179						new Point($X1 + $radius, $Y2),
180						$color,
181						$lineWidth,
182						$lineDotSize,
183						$shadowProperties);
184
185		$this->drawLine(new Point($X1, $Y2 - $radius),
186						new Point($X1, $Y1 + $radius),
187						$color,
188						$lineWidth,
189						$lineDotSize,
190						$shadowProperties);
191	}
192
193	/**
194	 * This function creates a filled rectangle with rounded corners
195	 * and antialiasing
196	 */
197	function drawFilledRoundedRectangle(Point $point1, Point $point2, $radius,
198										Color $color, $lineWidth, $lineDotSize,
199										ShadowProperties $shadowProperties) {
200		$C_Rectangle = $this->allocateColor($color);
201
202		$Step = 90 / ((M_PI * $radius) / 2);
203
204		for($i = 0; $i <= 90; $i = $i + $Step) {
205			$Xi1 = cos ( ($i + 180) * M_PI / 180 ) * $radius
206				+ $point1->getX()
207				+ $radius;
208
209			$Yi1 = sin ( ($i + 180) * M_PI / 180 ) * $radius
210				+ $point1->getY()
211				+ $radius;
212
213			$Xi2 = cos ( ($i - 90) * M_PI / 180 ) * $radius
214				+ $point2->getX()
215				- $radius;
216
217			$Yi2 = sin ( ($i - 90) * M_PI / 180 ) * $radius
218				+ $point1->getY()
219				+ $radius;
220
221			$Xi3 = cos ( ($i) * M_PI / 180 ) * $radius
222				+ $point2->getX()
223				- $radius;
224
225			$Yi3 = sin ( ($i) * M_PI / 180 ) * $radius
226				+ $point2->getY()
227				- $radius;
228
229			$Xi4 = cos ( ($i + 90) * M_PI / 180 ) * $radius
230				+ $point1->getX()
231				+ $radius;
232
233			$Yi4 = sin ( ($i + 90) * M_PI / 180 ) * $radius
234				+ $point2->getY()
235				- $radius;
236
237			imageline($this->picture,
238					  $Xi1, $Yi1,
239					  $point1->getX() + $radius, $Yi1,
240					  $C_Rectangle);
241
242			imageline($this->picture, $point2->getX() - $radius, $Yi2,
243					  $Xi2, $Yi2,
244					  $C_Rectangle);
245
246			imageline($this->picture,
247					  $point2->getX() - $radius, $Yi3,
248					  $Xi3, $Yi3,
249					  $C_Rectangle);
250
251			imageline($this->picture,
252					  $Xi4, $Yi4,
253					  $point1->getX() + $radius, $Yi4,
254					  $C_Rectangle );
255
256			$this->drawAntialiasPixel(new Point($Xi1, $Yi1),
257									  $color,
258									  $shadowProperties);
259			$this->drawAntialiasPixel(new Point($Xi2, $Yi2),
260									  $color,
261									  $shadowProperties);
262			$this->drawAntialiasPixel(new Point($Xi3, $Yi3),
263									  $color,
264									  $shadowProperties);
265			$this->drawAntialiasPixel(new Point($Xi4, $Yi4),
266									  $color,
267									  $shadowProperties);
268		}
269
270		imagefilledrectangle($this->picture,
271							 $point1->getX(), $point1->getY() + $radius,
272							 $point2->getX(), $point2->getY() - $radius,
273							 $C_Rectangle);
274
275		imagefilledrectangle($this->picture,
276							 $point1->getX() + $radius, $point1->getY(),
277							 $point2->getX() - $radius, $point2->getY(),
278							 $C_Rectangle);
279
280		$X1 = $point1->getX() - .2;
281		$Y1 = $point1->getY() - .2;
282		$X2 = $point2->getX() + .2;
283		$Y2 = $point2->getY() + .2;
284		$this->drawLine(new Point($X1 + $radius, $Y1),
285						new Point($X2 - $radius, $Y1),
286						$color,
287						$lineWidth, $lineDotSize,
288						$shadowProperties);
289
290		$this->drawLine(new Point($X2, $Y1 + $radius),
291						new Point($X2, $Y2 - $radius),
292						$color,
293						$lineWidth, $lineDotSize,
294						$shadowProperties);
295
296		$this->drawLine(new Point($X2 - $radius, $Y2),
297						new Point($X1 + $radius, $Y2),
298						$color,
299						$lineWidth, $lineDotSize,
300						$shadowProperties);
301
302		$this->drawLine(new Point($X1, $Y2 - $radius),
303						new Point($X1, $Y1 + $radius),
304						$color,
305						$lineWidth, $lineDotSize,
306						$shadowProperties);
307
308	}
309
310
311	public function drawLine(Point $point1, Point $point2, Color $color, $lineWidth, $lineDotSize, ShadowProperties $shadowProperties, Point $boundingBoxMin = null, Point $boundingBoxMax = null) {
312		if ($lineDotSize > 1) {
313			$this->drawDottedLine($point1,
314								  $point2,
315								  $lineDotSize, $lineWidth,
316								  $color, $shadowProperties,
317								  $boundingBoxMin,
318								  $boundingBoxMax);
319			return;
320		}
321
322		$Distance = $point1->distanceFrom($point2);
323		if ($Distance == 0)
324			return;
325		$XStep = ($point2->getX() - $point1->getX()) / $Distance;
326		$YStep = ($point2->getY() - $point1->getY()) / $Distance;
327
328		for($i = 0; $i <= $Distance; $i ++) {
329			$X = $i * $XStep + $point1->getX();
330			$Y = $i * $YStep + $point1->getY();
331
332			if ((($boundingBoxMin == null) || (($X >= $boundingBoxMin->getX())
333											   && ($Y >= $boundingBoxMin->getY())))
334				&& (($boundingBoxMax == null) || (($X <= $boundingBoxMax->getX())
335												  && ($Y <= $boundingBoxMax->getY())))) {
336				if ($lineWidth == 1)
337					$this->drawAntialiasPixel(new Point($X, $Y), $color, $shadowProperties);
338				else {
339					$StartOffset = - ($lineWidth / 2);
340					$EndOffset = ($lineWidth / 2);
341					for($j = $StartOffset; $j <= $EndOffset; $j ++)
342						$this->drawAntialiasPixel(new Point($X + $j, $Y + $j),
343												  $color, $shadowProperties);
344				}
345			}
346		}
347	}
348
349	public function drawDottedLine(Point $point1, Point $point2, $dotSize, $lineWidth, Color $color, ShadowProperties $shadowProperties, Point $boundingBoxMin = null, Point $boundingBoxMax = null) {
350		$Distance = $point1->distanceFrom($point2);
351
352		$XStep = ($point2->getX() - $point1->getX()) / $Distance;
353		$YStep = ($point2->getY() - $point1->getY()) / $Distance;
354
355		$DotIndex = 0;
356		for($i = 0; $i <= $Distance; $i ++) {
357			$X = $i * $XStep + $point1->getX();
358			$Y = $i * $YStep + $point1->getY();
359
360			if ($DotIndex <= $dotSize) {
361				if (($boundingBoxMin == null || (($X >= $boundingBoxMin->getX())
362												 && ($Y >= $boundingBoxMin->getY())))
363					&& ($boundingBoxMax == null || (($X <= $boundingBoxMax->getX())
364													&& ($Y <= $boundingBoxMax->getY())))) {
365					if ($lineWidth == 1)
366						$this->drawAntialiasPixel(new Point($X, $Y),
367												  $color, $shadowProperties);
368					else {
369						$StartOffset = - ($lineWidth / 2);
370						$EndOffset = ($lineWidth / 2);
371						for($j = $StartOffset; $j <= $EndOffset; $j ++) {
372							$this->drawAntialiasPixel(new Point($X + $j,
373																$Y + $j),
374													  $color, $shadowProperties);
375						}
376					}
377				}
378			}
379
380			$DotIndex ++;
381			if ($DotIndex == $dotSize * 2)
382				$DotIndex = 0;
383		}
384	}
385
386	public function drawAntialiasPixel(Point $point, Color $color, ShadowProperties $shadowProperties, $alpha = 100) {
387		/* Process shadows */
388		if ($shadowProperties->active) {
389			$this->drawAntialiasPixel(new Point($point->getX() + $shadowProperties->xDistance,
390												$point->getY() + $shadowProperties->yDistance),
391									  $shadowProperties->color,
392									  ShadowProperties::NoShadow(),
393									  $shadowProperties->alpha);
394			if ($shadowProperties->blur != 0) {
395				$AlphaDecay = ($shadowProperties->alpha / $shadowProperties->blur);
396
397				for($i = 1; $i <= $shadowProperties->blur; $i ++)
398					$this->drawAntialiasPixel(new Point($point->getX() + $shadowProperties->xDistance - $i / 2,
399														$point->getY() + $shadowProperties->yDistance - $i / 2),
400											  $shadowProperties->color,
401											  ShadowProperties::NoShadow(),
402											  $shadowProperties->alpha - $AlphaDecay * $i);
403				for($i = 1; $i <= $shadowProperties->blur; $i ++)
404					$this->drawAntialiasPixel(new Point($point->getX() + $shadowProperties->xDistance + $i / 2,
405														$point->getY() + $shadowProperties->yDistance + $i / 2),
406											  $shadowProperties->color,
407											  ShadowProperties::NoShadow(),
408											  $shadowProperties->alpha - $AlphaDecay * $i);
409			}
410		}
411
412		$Xi = floor ( $point->getX() );
413		$Yi = floor ( $point->getY() );
414
415		if ($Xi == $point->getX() && $Yi == $point->getY()) {
416			if ($alpha == 100) {
417				$C_Aliased = $this->allocateColor($color);
418				imagesetpixel ( $this->picture,
419								$point->getX(), $point->getY(),
420								$C_Aliased );
421			} else
422				$this->drawAlphaPixel($point, $alpha, $color);
423		} else {
424			$Alpha1 = (((1 - ($point->getX() - $Xi)) * (1 - ($point->getY() - $Yi)) * 100) / 100) * $alpha;
425			if ($Alpha1 > $this->antialiasQuality) {
426				$this->drawAlphaPixel(new Point($Xi, $Yi), $Alpha1, $color);
427			}
428
429			$Alpha2 = ((($point->getX() - $Xi) * (1 - ($point->getY() - $Yi)) * 100) / 100) * $alpha;
430			if ($Alpha2 > $this->antialiasQuality) {
431				$this->drawAlphaPixel (new Point($Xi + 1, $Yi), $Alpha2, $color);
432			}
433
434			$Alpha3 = (((1 - ($point->getX() - $Xi)) * ($point->getY() - $Yi) * 100) / 100)
435				* $alpha;
436			if ($Alpha3 > $this->antialiasQuality) {
437				$this->drawAlphaPixel (new Point($Xi, $Yi + 1), $Alpha3, $color);
438			}
439
440			$Alpha4 = ((($point->getX() - $Xi) * ($point->getY() - $Yi) * 100) / 100)
441				* $alpha;
442			if ($Alpha4 > $this->antialiasQuality) {
443				$this->drawAlphaPixel (new Point($Xi + 1, $Yi + 1), $Alpha4, $color);
444			}
445		}
446	}
447
448	public function drawAlphaPixel(Point $point, $alpha, Color $color) {
449		/** @todo Check that the point is within the bounds of the
450		 * canvas */
451
452		$RGB2 = imagecolorat ( $this->picture, $point->getX(), $point->getY());
453		$R2 = ($RGB2 >> 16) & 0xFF;
454		$G2 = ($RGB2 >> 8) & 0xFF;
455		$B2 = $RGB2 & 0xFF;
456
457		$iAlpha = (100 - $alpha) / 100;
458		$alpha = $alpha / 100;
459
460		$Ra = floor ( $color->r * $alpha + $R2 * $iAlpha );
461		$Ga = floor ( $color->g * $alpha + $G2 * $iAlpha );
462		$Ba = floor ( $color->b * $alpha + $B2 * $iAlpha );
463
464		$C_Aliased = $this->allocateColor (new Color($Ra, $Ga, $Ba));
465		imagesetpixel($this->picture, $point->getX(), $point->getY(), $C_Aliased );
466	}
467
468	/**
469	 * Color helper
470	 *
471	 * @todo This shouldn't need to be public, it's only a temporary
472	 * step while refactoring
473	 */
474	public function allocateColor(Color $color, $Factor = 0, $alpha = 100) {
475		if ($Factor != 0) {
476			$color = $color->addRGBIncrement($Factor);
477		}
478
479		if ($alpha == 100) {
480			return (imagecolorallocate ($this->picture, $color->r, $color->g, $color->b ));
481		}
482		else {
483			return imagecolorallocatealpha($this->picture,
484										   $color->r,
485										   $color->g,
486										   $color->b,
487										   127 * (1 - $alpha / 100));
488		}
489	}
490
491	/**
492	 * @todo This is only a temporary interface while I'm
493	 * refactoring. This should eventually be removed.
494	 */
495	public function getPicture() {
496		return $this->picture;
497	}
498
499	public function getAntialiasQuality() {
500		return $this->antialiasQuality;
501	}
502
503	public function setAntialiasQuality($newQuality) {
504		if (!is_numeric($newQuality)
505			|| $newQuality < 0
506			|| $newQuality > 100) {
507			throw new InvalidArgumentException("Invalid argument to GDCanvas::setAntialiasQuality()");
508		}
509
510		$this->antialiasQuality = $newQuality;
511	}
512
513	function drawText($fontSize, $angle, Point $point, Color $color, $fontName, $text, ShadowProperties $shadowProperties) {
514		if ($shadowProperties->active) {
515			$gdShadowColor = $this->allocateColor($shadowProperties->color);
516
517			imagettftext($this->picture, $fontSize, $angle,
518						 $point->getX() + $shadowProperties->xDistance,
519						 $point->getY() + $shadowProperties->yDistance,
520						 $gdShadowColor,
521						 $fontName, $text);
522		}
523
524		$gdColor = $this->allocateColor($color);
525
526		imagettftext($this->picture, $fontSize, $angle,
527					 $point->getX(), $point->getY(),
528					 $gdColor, $fontName, $text);
529	}
530
531	function drawCircle(Point $center, $height, Color $color, ShadowProperties $shadowProperties, $width = null) {
532		if ($width == null) {
533			$width = $height;
534		}
535
536		$Step = 360 / (2 * M_PI * max ( $width, $height ));
537
538		for($i = 0; $i <= 360; $i = $i + $Step) {
539			$X = cos ( $i * M_PI / 180 ) * $height + $center->getX();
540			$Y = sin ( $i * M_PI / 180 ) * $width + $center->getY();
541			$this->drawAntialiasPixel(new Point($X, $Y),
542									  $color,
543									  $shadowProperties);
544		}
545
546	}
547
548	public function drawFilledCircle(Point $center, $height, Color $color, ShadowProperties $shadowProperties, $width = null) {
549		if ($width == null) {
550			$width = $height;
551		}
552
553		$C_Circle = $this->allocateColor($color);
554		$Step = 360 / (2 * M_PI * max ( $width, $height ));
555
556		for($i = 90; $i <= 270; $i = $i + $Step) {
557			$X1 = cos ( $i * M_PI / 180 ) * $height + $center->getX();
558			$Y1 = sin ( $i * M_PI / 180 ) * $width + $center->getY();
559			$X2 = cos ( (180 - $i) * M_PI / 180 ) * $height + $center->getX();
560			$Y2 = sin ( (180 - $i) * M_PI / 180 ) * $width + $center->getY();
561
562			$this->drawAntialiasPixel(new Point($X1 - 1, $Y1 - 1),
563									  $color,
564									  $shadowProperties);
565			$this->drawAntialiasPixel(new Point($X2 - 1, $Y2 - 1),
566									  $color,
567									  $shadowProperties);
568
569			if (($Y1 - 1) > $center->getY() - max ( $width, $height )) {
570				imageline ( $this->picture, $X1, $Y1 - 1, $X2 - 1, $Y2 - 1, $C_Circle );
571			}
572		}
573	}
574
575	function drawFilledPolygon(array $points, $numPoints, Color $color, $alpha = 100) {
576		$gdColor = $this->allocateColor($color, 0, $alpha);
577
578		imagefilledpolygon($this->picture,
579						   $points,
580						   $numPoints,
581						   $gdColor);
582	}
583
584	private $picture;
585
586	/**
587	 * Quality of the antialiasing we do: 0 is maximum, 100 is minimum
588	 */
589	private $antialiasQuality;
590}
591