1<?php
2/**
3 * @package php-svg-lib
4 * @link    http://github.com/PhenX/php-svg-lib
5 * @author  Fabien M�nager <fabien.menager@gmail.com>
6 * @license GNU LGPLv3+ http://www.gnu.org/copyleft/lesser.html
7 */
8
9namespace Svg;
10
11use Svg\Tag\AbstractTag;
12
13class Style
14{
15    const TYPE_COLOR = 1;
16    const TYPE_LENGTH = 2;
17    const TYPE_NAME = 3;
18    const TYPE_ANGLE = 4;
19    const TYPE_NUMBER = 5;
20
21    public $color;
22    public $opacity;
23    public $display;
24
25    public $fill;
26    public $fillOpacity;
27    public $fillRule;
28
29    public $stroke;
30    public $strokeOpacity;
31    public $strokeLinecap;
32    public $strokeLinejoin;
33    public $strokeMiterlimit;
34    public $strokeWidth;
35    public $strokeDasharray;
36    public $strokeDashoffset;
37
38    public $fontFamily = 'serif';
39    public $fontSize = 12;
40    public $fontWeight = 'normal';
41    public $fontStyle = 'normal';
42    public $textAnchor = 'start';
43
44    protected function getStyleMap()
45    {
46        return array(
47            'color'             => array('color', self::TYPE_COLOR),
48            'opacity'           => array('opacity', self::TYPE_NUMBER),
49            'display'           => array('display', self::TYPE_NAME),
50
51            'fill'              => array('fill', self::TYPE_COLOR),
52            'fill-opacity'      => array('fillOpacity', self::TYPE_NUMBER),
53            'fill-rule'         => array('fillRule', self::TYPE_NAME),
54
55            'stroke'            => array('stroke', self::TYPE_COLOR),
56            'stroke-dasharray'  => array('strokeDasharray', self::TYPE_NAME),
57            'stroke-dashoffset' => array('strokeDashoffset', self::TYPE_NUMBER),
58            'stroke-linecap'    => array('strokeLinecap', self::TYPE_NAME),
59            'stroke-linejoin'   => array('strokeLinejoin', self::TYPE_NAME),
60            'stroke-miterlimit' => array('strokeMiterlimit', self::TYPE_NUMBER),
61            'stroke-opacity'    => array('strokeOpacity', self::TYPE_NUMBER),
62            'stroke-width'      => array('strokeWidth', self::TYPE_NUMBER),
63
64            'font-family'       => array('fontFamily', self::TYPE_NAME),
65            'font-size'         => array('fontSize', self::TYPE_NUMBER),
66            'font-weight'       => array('fontWeight', self::TYPE_NAME),
67            'font-style'        => array('fontStyle', self::TYPE_NAME),
68            'text-anchor'       => array('textAnchor', self::TYPE_NAME),
69        );
70    }
71
72    /**
73     * @param $attributes
74     *
75     * @return Style
76     */
77    public function fromAttributes($attributes)
78    {
79        $this->fillStyles($attributes);
80
81        if (isset($attributes["style"])) {
82            $styles = self::parseCssStyle($attributes["style"]);
83            $this->fillStyles($styles);
84        }
85    }
86
87    public function inherit(AbstractTag $tag) {
88        $group = $tag->getParentGroup();
89        if ($group) {
90            $parent_style = $group->getStyle();
91
92            foreach ($parent_style as $_key => $_value) {
93                if ($_value !== null) {
94                    $this->$_key = $_value;
95                }
96            }
97        }
98    }
99
100    public function fromStyleSheets(AbstractTag $tag, $attributes) {
101        $class = isset($attributes["class"]) ? preg_split('/\s+/', trim($attributes["class"])) : null;
102
103        $stylesheets = $tag->getDocument()->getStyleSheets();
104
105        $styles = array();
106
107        foreach ($stylesheets as $_sc) {
108
109            /** @var \Sabberworm\CSS\RuleSet\DeclarationBlock $_decl */
110            foreach ($_sc->getAllDeclarationBlocks() as $_decl) {
111
112                /** @var \Sabberworm\CSS\Property\Selector $_selector */
113                foreach ($_decl->getSelectors() as $_selector) {
114                    $_selector = $_selector->getSelector();
115
116                    // Match class name
117                    if ($class !== null) {
118                        foreach ($class as $_class) {
119                            if ($_selector === ".$_class") {
120                                /** @var \Sabberworm\CSS\Rule\Rule $_rule */
121                                foreach ($_decl->getRules() as $_rule) {
122                                    $styles[$_rule->getRule()] = $_rule->getValue() . "";
123                                }
124
125                                break 2;
126                            }
127                        }
128                    }
129
130                    // Match tag name
131                    if ($_selector === $tag->tagName) {
132                        /** @var \Sabberworm\CSS\Rule\Rule $_rule */
133                        foreach ($_decl->getRules() as $_rule) {
134                            $styles[$_rule->getRule()] = $_rule->getValue() . "";
135                        }
136
137                        break;
138                    }
139                }
140            }
141        }
142
143        $this->fillStyles($styles);
144    }
145
146    protected function fillStyles($styles)
147    {
148        foreach ($this->getStyleMap() as $from => $spec) {
149            if (isset($styles[$from])) {
150                list($to, $type) = $spec;
151                $value = null;
152                switch ($type) {
153                    case self::TYPE_COLOR:
154                        $value = self::parseColor($styles[$from]);
155                        break;
156
157                    case self::TYPE_NUMBER:
158                        $value = ($styles[$from] === null) ? null : (float)$styles[$from];
159                        break;
160
161                    default:
162                        $value = $styles[$from];
163                }
164
165                if ($value !== null) {
166                    $this->$to = $value;
167                }
168            }
169        }
170    }
171
172    static function parseColor($color)
173    {
174        $color = strtolower(trim($color));
175
176        $parts = preg_split('/[^,]\s+/', $color, 2);
177
178        if (count($parts) == 2) {
179            $color = $parts[1];
180        }
181        else {
182            $color = $parts[0];
183        }
184
185        if ($color === "none") {
186            return "none";
187        }
188
189        // SVG color name
190        if (isset(self::$colorNames[$color])) {
191            return self::parseHexColor(self::$colorNames[$color]);
192        }
193
194        // Hex color
195        if ($color[0] === "#") {
196            return self::parseHexColor($color);
197        }
198
199        // RGB color
200        if (strpos($color, "rgb") !== false) {
201            return self::getTriplet($color);
202        }
203
204        // RGB color
205        if (strpos($color, "hsl") !== false) {
206            $triplet = self::getTriplet($color, true);
207
208            if ($triplet == null) {
209                return null;
210            }
211
212            list($h, $s, $l) = $triplet;
213
214            $r = $l;
215            $g = $l;
216            $b = $l;
217            $v = ($l <= 0.5) ? ($l * (1.0 + $s)) : ($l + $s - $l * $s);
218            if ($v > 0) {
219                $m = $l + $l - $v;
220                $sv = ($v - $m) / $v;
221                $h *= 6.0;
222                $sextant = floor($h);
223                $fract = $h - $sextant;
224                $vsf = $v * $sv * $fract;
225                $mid1 = $m + $vsf;
226                $mid2 = $v - $vsf;
227
228                switch ($sextant) {
229                    case 0:
230                        $r = $v;
231                        $g = $mid1;
232                        $b = $m;
233                        break;
234                    case 1:
235                        $r = $mid2;
236                        $g = $v;
237                        $b = $m;
238                        break;
239                    case 2:
240                        $r = $m;
241                        $g = $v;
242                        $b = $mid1;
243                        break;
244                    case 3:
245                        $r = $m;
246                        $g = $mid2;
247                        $b = $v;
248                        break;
249                    case 4:
250                        $r = $mid1;
251                        $g = $m;
252                        $b = $v;
253                        break;
254                    case 5:
255                        $r = $v;
256                        $g = $m;
257                        $b = $mid2;
258                        break;
259                }
260            }
261
262            return array(
263                $r * 255.0,
264                $g * 255.0,
265                $b * 255.0,
266            );
267        }
268
269        // Gradient
270        if (strpos($color, "url(#") !== false) {
271            $i = strpos($color, "(");
272            $j = strpos($color, ")");
273
274            // Bad url format
275            if ($i === false || $j === false) {
276                return null;
277            }
278
279            return trim(substr($color, $i + 1, $j - $i - 1));
280        }
281
282        return null;
283    }
284
285    static function getTriplet($color, $percent = false) {
286        $i = strpos($color, "(");
287        $j = strpos($color, ")");
288
289        // Bad color value
290        if ($i === false || $j === false) {
291            return null;
292        }
293
294        $triplet = preg_split("/\\s*,\\s*/", trim(substr($color, $i + 1, $j - $i - 1)));
295
296        if (count($triplet) != 3) {
297            return null;
298        }
299
300        foreach (array_keys($triplet) as $c) {
301            $triplet[$c] = trim($triplet[$c]);
302
303            if ($percent) {
304                if ($triplet[$c][strlen($triplet[$c]) - 1] === "%") {
305                    $triplet[$c] = $triplet[$c] / 100;
306                }
307                else {
308                    $triplet[$c] = $triplet[$c] / 255;
309                }
310            }
311            else {
312                if ($triplet[$c][strlen($triplet[$c]) - 1] === "%") {
313                    $triplet[$c] = round($triplet[$c] * 2.55);
314                }
315            }
316        }
317
318        return $triplet;
319    }
320
321    static function parseHexColor($hex)
322    {
323        $c = array(0, 0, 0);
324
325        // #FFFFFF
326        if (isset($hex[6])) {
327            $c[0] = hexdec(substr($hex, 1, 2));
328            $c[1] = hexdec(substr($hex, 3, 2));
329            $c[2] = hexdec(substr($hex, 5, 2));
330        } else {
331            $c[0] = hexdec($hex[1] . $hex[1]);
332            $c[1] = hexdec($hex[2] . $hex[2]);
333            $c[2] = hexdec($hex[3] . $hex[3]);
334        }
335
336        return $c;
337    }
338
339    /**
340     * Simple CSS parser
341     *
342     * @param $style
343     *
344     * @return array
345     */
346    static function parseCssStyle($style)
347    {
348        $matches = array();
349        preg_match_all("/([a-z-]+)\\s*:\\s*([^;$]+)/si", $style, $matches, PREG_SET_ORDER);
350
351        $styles = array();
352        foreach ($matches as $match) {
353            $styles[$match[1]] = $match[2];
354        }
355
356        return $styles;
357    }
358
359    /**
360     * Convert a size to a float
361     *
362     * @param string $size          SVG size
363     * @param float  $dpi           DPI
364     * @param float  $referenceSize Reference size
365     *
366     * @return float|null
367     */
368    static function convertSize($size, $referenceSize = 11.0, $dpi = 96.0) {
369        $size = trim(strtolower($size));
370
371        if (is_numeric($size)) {
372            return $size;
373        }
374
375        if ($pos = strpos($size, "px")) {
376            return floatval(substr($size, 0, $pos));
377        }
378
379        if ($pos = strpos($size, "pt")) {
380            return floatval(substr($size, 0, $pos));
381        }
382
383        if ($pos = strpos($size, "cm")) {
384            return floatval(substr($size, 0, $pos)) * $dpi;
385        }
386
387        if ($pos = strpos($size, "%")) {
388            return $referenceSize * substr($size, 0, $pos) / 100;
389        }
390
391        if ($pos = strpos($size, "em")) {
392            return $referenceSize * substr($size, 0, $pos);
393        }
394
395        // TODO cm, mm, pc, in, etc
396
397        return null;
398    }
399
400    static $colorNames = array(
401        'antiquewhite'         => '#FAEBD7',
402        'aqua'                 => '#00FFFF',
403        'aquamarine'           => '#7FFFD4',
404        'beige'                => '#F5F5DC',
405        'black'                => '#000000',
406        'blue'                 => '#0000FF',
407        'brown'                => '#A52A2A',
408        'cadetblue'            => '#5F9EA0',
409        'chocolate'            => '#D2691E',
410        'cornflowerblue'       => '#6495ED',
411        'crimson'              => '#DC143C',
412        'darkblue'             => '#00008B',
413        'darkgoldenrod'        => '#B8860B',
414        'darkgreen'            => '#006400',
415        'darkmagenta'          => '#8B008B',
416        'darkorange'           => '#FF8C00',
417        'darkred'              => '#8B0000',
418        'darkseagreen'         => '#8FBC8F',
419        'darkslategray'        => '#2F4F4F',
420        'darkviolet'           => '#9400D3',
421        'deepskyblue'          => '#00BFFF',
422        'dodgerblue'           => '#1E90FF',
423        'firebrick'            => '#B22222',
424        'forestgreen'          => '#228B22',
425        'fuchsia'              => '#FF00FF',
426        'gainsboro'            => '#DCDCDC',
427        'gold'                 => '#FFD700',
428        'gray'                 => '#808080',
429        'green'                => '#008000',
430        'greenyellow'          => '#ADFF2F',
431        'hotpink'              => '#FF69B4',
432        'indigo'               => '#4B0082',
433        'khaki'                => '#F0E68C',
434        'lavenderblush'        => '#FFF0F5',
435        'lemonchiffon'         => '#FFFACD',
436        'lightcoral'           => '#F08080',
437        'lightgoldenrodyellow' => '#FAFAD2',
438        'lightgreen'           => '#90EE90',
439        'lightsalmon'          => '#FFA07A',
440        'lightskyblue'         => '#87CEFA',
441        'lightslategray'       => '#778899',
442        'lightyellow'          => '#FFFFE0',
443        'lime'                 => '#00FF00',
444        'limegreen'            => '#32CD32',
445        'magenta'              => '#FF00FF',
446        'maroon'               => '#800000',
447        'mediumaquamarine'     => '#66CDAA',
448        'mediumorchid'         => '#BA55D3',
449        'mediumseagreen'       => '#3CB371',
450        'mediumspringgreen'    => '#00FA9A',
451        'mediumvioletred'      => '#C71585',
452        'midnightblue'         => '#191970',
453        'mintcream'            => '#F5FFFA',
454        'moccasin'             => '#FFE4B5',
455        'navy'                 => '#000080',
456        'olive'                => '#808000',
457        'orange'               => '#FFA500',
458        'orchid'               => '#DA70D6',
459        'palegreen'            => '#98FB98',
460        'palevioletred'        => '#D87093',
461        'peachpuff'            => '#FFDAB9',
462        'pink'                 => '#FFC0CB',
463        'powderblue'           => '#B0E0E6',
464        'purple'               => '#800080',
465        'red'                  => '#FF0000',
466        'royalblue'            => '#4169E1',
467        'salmon'               => '#FA8072',
468        'seagreen'             => '#2E8B57',
469        'sienna'               => '#A0522D',
470        'silver'               => '#C0C0C0',
471        'skyblue'              => '#87CEEB',
472        'slategray'            => '#708090',
473        'springgreen'          => '#00FF7F',
474        'steelblue'            => '#4682B4',
475        'tan'                  => '#D2B48C',
476        'teal'                 => '#008080',
477        'thistle'              => '#D8BFD8',
478        'turquoise'            => '#40E0D0',
479        'violetred'            => '#D02090',
480        'white'                => '#FFFFFF',
481        'yellow'               => '#FFFF00',
482        'aliceblue'            => '#f0f8ff',
483        'azure'                => '#f0ffff',
484        'bisque'               => '#ffe4c4',
485        'blanchedalmond'       => '#ffebcd',
486        'blueviolet'           => '#8a2be2',
487        'burlywood'            => '#deb887',
488        'chartreuse'           => '#7fff00',
489        'coral'                => '#ff7f50',
490        'cornsilk'             => '#fff8dc',
491        'cyan'                 => '#00ffff',
492        'darkcyan'             => '#008b8b',
493        'darkgray'             => '#a9a9a9',
494        'darkgrey'             => '#a9a9a9',
495        'darkkhaki'            => '#bdb76b',
496        'darkolivegreen'       => '#556b2f',
497        'darkorchid'           => '#9932cc',
498        'darksalmon'           => '#e9967a',
499        'darkslateblue'        => '#483d8b',
500        'darkslategrey'        => '#2f4f4f',
501        'darkturquoise'        => '#00ced1',
502        'deeppink'             => '#ff1493',
503        'dimgray'              => '#696969',
504        'dimgrey'              => '#696969',
505        'floralwhite'          => '#fffaf0',
506        'ghostwhite'           => '#f8f8ff',
507        'goldenrod'            => '#daa520',
508        'grey'                 => '#808080',
509        'honeydew'             => '#f0fff0',
510        'indianred'            => '#cd5c5c',
511        'ivory'                => '#fffff0',
512        'lavender'             => '#e6e6fa',
513        'lawngreen'            => '#7cfc00',
514        'lightblue'            => '#add8e6',
515        'lightcyan'            => '#e0ffff',
516        'lightgray'            => '#d3d3d3',
517        'lightgrey'            => '#d3d3d3',
518        'lightpink'            => '#ffb6c1',
519        'lightseagreen'        => '#20b2aa',
520        'lightslategrey'       => '#778899',
521        'lightsteelblue'       => '#b0c4de',
522        'linen'                => '#faf0e6',
523        'mediumblue'           => '#0000cd',
524        'mediumpurple'         => '#9370db',
525        'mediumslateblue'      => '#7b68ee',
526        'mediumturquoise'      => '#48d1cc',
527        'mistyrose'            => '#ffe4e1',
528        'navajowhite'          => '#ffdead',
529        'oldlace'              => '#fdf5e6',
530        'olivedrab'            => '#6b8e23',
531        'orangered'            => '#ff4500',
532        'palegoldenrod'        => '#eee8aa',
533        'paleturquoise'        => '#afeeee',
534        'papayawhip'           => '#ffefd5',
535        'peru'                 => '#cd853f',
536        'plum'                 => '#dda0dd',
537        'rosybrown'            => '#bc8f8f',
538        'saddlebrown'          => '#8b4513',
539        'sandybrown'           => '#f4a460',
540        'seashell'             => '#fff5ee',
541        'slateblue'            => '#6a5acd',
542        'slategrey'            => '#708090',
543        'snow'                 => '#fffafa',
544        'tomato'               => '#ff6347',
545        'violet'               => '#ee82ee',
546        'wheat'                => '#f5deb3',
547        'whitesmoke'           => '#f5f5f5',
548        'yellowgreen'          => '#9acd32',
549    );
550}