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