source: spip-zone/_plugins_/adaptive_images/trunk/lib/AdaptiveImages/AdaptiveImages.php @ 119773

Last change on this file since 119773 was 119773, checked in by Cerdic, 8 months ago

Fix bug sur les picture qui necessitent un display block

File size: 58.5 KB
Line 
1<?php
2/**
3 * AdaptiveImages
4 *
5 * @version    2.1.0
6 * @copyright  2013-2020
7 * @author     Nursit
8 * @licence    GNU/GPL3
9 * @source     https://github.com/nursit/AdaptiveImages
10 */
11
12
13class AdaptiveImages {
14        /**
15         * @var Array
16         */
17        static protected $instances = array();
18
19        /**
20         * Use progressive rendering for PNG and GIF when JS disabled ?
21         * @var boolean
22         */
23        protected $nojsPngGifProgressiveRendering = false;
24
25        /**
26         * Background color for JPG lowsrc generation
27         * (if source has transparency layer)
28         * @var string
29         */
30        protected $lowsrcJpgBgColor = '#ffffff';
31
32
33        /**
34         * JPG compression quality for JPG lowsrc
35         * @var int
36         */
37        protected $lowsrcJpgQuality = 40;
38
39        /**
40         * JPG compression quality for 1x JPG images
41         * @var int
42         */
43        protected $x10JpgQuality = 75;
44
45        /**
46         * JPG compression quality for 1.5x JPG images
47         * @var int
48         */
49        protected $x15JpgQuality = 65;
50
51        /**
52         * JPG compression quality for 2x JPG images
53         * @var int
54         */
55        protected $x20JpgQuality = 45;
56
57        /**
58         * Breakpoints width for image generation
59         * @var array
60         */
61        protected $defaultBkpts = array(480,960,1440);
62
63        /**
64         * Maximum display width for images
65         * @var int
66         */
67        protected $maxWidth1x = 640;
68
69        /**
70         * Minimum display width for adaptive images (smaller will be unchanged)
71         * @var int
72         */
73        protected $minWidth1x = 480;
74
75        /**
76         * Minimum filesize for adaptive images (smaller will be unchanged)
77         * @var int
78         */
79        protected $minFileSize = 20480; // 20ko
80
81        /**
82         * Maximum width for delivering mobile version in data-src-mobile=""
83         * @var int
84         */
85        protected $maxWidthMobileVersion = 480;
86
87        /**
88         * target width for fallback thumbnail
89         * @var int
90         */
91        protected $lowsrcWidth = 160;
92
93        /**
94         * Set to true to generate adapted image only at first request from users
95         * (speed up initial page generation)
96         * @var int
97         */
98        protected $onDemandImages = false;
99
100
101        /**
102         * Allowed format images to be adapted
103         * @var array
104         */
105        protected $acceptedFormats = array('gif','png','jpeg','jpg');
106
107        /**
108         * directory for storing adaptive images
109         * @var string
110         */
111        protected $destDirectory = "local/adapt-img/";
112
113        /**
114         * Maximum number of px for image that can be loaded in memory by GD
115         * can be used to avoid Fatal Memory Error on large image if PHP memory limited
116         * @var string
117         */
118        protected $maxImagePxGDMemoryLimit = 0;
119
120        /**
121         * Choose the markup Methd : '3layers' (default) or 'srcset'
122         * @var string
123         */
124        protected $markupMethod = '3layers';
125
126        /**
127         * Should images always be in an instrinsic container, even if using srcset method?
128         * @var bool
129         */
130        protected $alwaysIntrinsic = true;
131
132        /**
133         * Set to true to delay loading with .lazy class on <html>
134         * need extra js to add .lazy on adapt-img-wrapper, remove .lazy on <html>
135         * and then remove .lazy on each .adapt-img-wrapper when visible
136         * @var int
137         */
138        protected $lazyload = false;
139
140        /**
141         * Name of a function to call to generate the thumbnail instead of the internal process
142         * @var string
143         */
144        protected $thumbnailGeneratorCallback = null;
145
146        /**
147         * Constructor
148         */
149        protected function __construct(){
150        }
151
152        /**
153         * get
154         * @param $property
155         * @return mixed
156         * @throws InvalidArgumentException
157         */
158        public function __get($property){
159                if(!property_exists($this,$property) OR $property=="instances") {
160      throw new InvalidArgumentException("Property {$property} doesn't exist");
161    }
162                return $this->{$property};
163        }
164
165        /**
166         * set
167         * @param $property
168         * @param $value
169         * @return mixed
170         * @throws InvalidArgumentException
171         */
172        public function __set($property, $value){
173                if(!property_exists($this,$property) OR $property=="instances") {
174      throw new InvalidArgumentException("Property {$property} doesn't exist");
175    }
176                if (in_array($property,array("nojsPngGifProgressiveRendering","onDemandImages","lazyload","alwaysIntrinsic"))){
177                        if (!is_bool($value))
178                                throw new InvalidArgumentException("Property {$property} needs a bool value");
179                }
180                elseif (in_array($property,array("lowsrcJpgBgColor","destDirectory","thumbnailGeneratorCallback","markupMethod"))){
181                        if (!is_string($value))
182                                throw new InvalidArgumentException("Property {$property} needs a string value");
183                }
184                elseif (in_array($property,array("defaultBkpts","acceptedFormats"))){
185                        if (!is_array($value))
186                                throw new InvalidArgumentException("Property {$property} needs an array value");
187                }
188                elseif (!is_int($value)){
189                        throw new InvalidArgumentException("Property {$property} needs an int value");
190                }
191                if ($property=="defaultBkpts"){
192                        sort($value);
193                }
194
195                return ($this->{$property} = $value);
196        }
197
198        /**
199         * Disable cloning
200         */
201        protected function __clone() {
202         trigger_error("Cannot clone a singleton class", E_USER_ERROR);
203        }
204
205        /**
206         * Retrieve the AdaptiveImages object
207         *
208         * @return AdaptiveImages
209         */
210        static public function getInstance() {
211                $class_name = (function_exists("get_called_class")?get_called_class():"AdaptiveImages");
212                if(!array_key_exists($class_name, self::$instances)) {
213      self::$instances[$class_name] = new $class_name();
214    }
215    return self::$instances[$class_name];
216        }
217
218        /**
219         * Log function for internal warning if we can avoid to throw an Exception
220         * Do nothing, should be overriden with your personal log function
221         * @param $message
222         */
223        protected function log($message){
224
225        }
226
227        /**
228         * Convert URL path to file system path
229         * By default just remove existing timestamp
230         * Should be overriden depending of your URL mapping rules vs DOCUMENT_ROOT
231         * can also remap Absolute URL of current website to filesystem path
232         * @param $url
233         * @return string
234         */
235        protected function URL2filepath($url){
236                // remove timestamp on URL
237                if (($p=strpos($url,'?'))!==FALSE)
238                        $url=substr($url,0,$p);
239
240                return $url;
241        }
242
243        /**
244         * Convert file system path to URL path
245         * By default just add timestamp for webperf issue
246         * Should be overriden depending of your URL mapping rules vs DOCUMENT_ROOT
247         * can map URL on specific domain (domain sharding for Webperf purpose)
248         * @param string $filepath
249         * @param bool $relative
250         * @return string
251         */
252        protected function filepath2URL($filepath, $relative=false){
253                // be carefull : maybe file doesn't exists yet (On demand generation)
254                if ($t = @filemtime($filepath))
255                        $filepath = "$filepath?$t";
256                return $filepath;
257        }
258
259        /**
260         * This hook allows to personalize markup depending on source img style and class attributes
261         * This do-noting method should be adapted to source markup generated by your CMS
262         *
263         * For instance : <img style="display:block;float:right" /> could be adapted in
264         * <span style="display:block;float:right"><span class="adapt-img-wrapper"><img class="adapt-img"/></span></span>
265         *
266         * @param string $markup
267         * @param string $originalClass
268         * @param string $originalStyle
269         * @return mixed
270         */
271        protected function imgMarkupHook(&$markup,$originalClass,$originalStyle){
272                return $markup;
273        }
274
275        /**
276         * Translate src of original image to URL subpath of adapted image
277         * the result will makes subdirectory of $destDirectory/320/10x/ and other variants
278         * the result must allow to retrive src from url in adaptedURLToSrc() methof
279         * @param string $src
280         * @return string
281         */
282        protected function adaptedSrcToURL($src){
283                $url = $this->filepath2URL($src, true);
284                if (($p=strpos($url,'?'))!==FALSE)
285                        $url=substr($url,0,$p);
286                // avoid / starting url : replace / by root/
287                if (strncmp($url,"/",1)==0)
288                        $url = "root".$url;
289                return $url;
290        }
291
292        /**
293         * Translate URL of subpath of adapted image to original image src
294         * This reverse the adaptedSrcToURL() method
295         * @param string $url
296         * @return string
297         */
298        protected function adaptedURLToSrc($url){
299                // replace root/ by /
300                if (strncmp($url,"root/",5)==0)
301                        $url = substr($url,4);
302                $src = $this->URL2filepath($url);
303                return $src;
304        }
305
306        /**
307         * Process the full HTML page :
308         *  - adapt all <img> in the HTML
309         *  - collect all inline <style> and put in the <head>
310         *  - add necessary JS
311         *
312         * @param string $html
313         *   HTML source page
314         * @param int $maxWidth1x
315         *   max display width for images 1x
316         * @param array|null $bkpt
317         * @return string
318         *  HTML modified page
319         */
320        public function adaptHTMLPage($html,$maxWidth1x=null,$bkpt=null){
321                // adapt all images that need it, if not already
322                $html = $this->adaptHTMLPart($html, $maxWidth1x, $bkpt);
323
324                // if there is adapted images in the page, add the necessary CSS and JS
325                if (strpos($html,"adapt-img-wrapper")!==false){
326                        $ins_style = "";
327                        // collect all adapt-img <style> in order to put it in the <head>
328                        preg_match_all(",(<style[^>]*adaptive[^>]*>(.*)</style>),Ums",$html,$matches);
329                        if (count($matches[2])){
330                                $html = str_replace($matches[1],"",$html);
331                                $ins_style .= "\n<style>".implode("\n",$matches[2])."\n</style>";
332                                // in case of this was only including <style>
333                                $html = str_replace("<!--[if !IE]><!--><!--<![endif]-->","",$html);
334                        }
335
336                        // Common styles for all adaptive images during loading
337
338                        $noscript = "";
339                        switch ($this->markupMethod) {
340                                case 'srcset':
341                                        $minwidthdesktop = $this->maxWidthMobileVersion + 0.5;
342                                        $base_style = "<style type='text/css'>"
343                                        ."img.adapt-img,.lazy img.adapt-img{max-width:100%;height:auto;}"
344                                        .".adapt-img-wrapper {display:block;max-width:100%;position:relative;background-position:center;-webkit-background-size:100% auto;-webkit-background-size:cover;background-size:cover;background-repeat:no-repeat;line-height:1px;overflow:hidden}"
345                                        .".adapt-img-wrapper.intrinsic::before{content:'';display:block;height:0;width:100%;}.adapt-img-wrapper.intrinsic img{position:absolute;left:0;top:0;width:100%;height:auto;}"
346                                        ."@media (min-width:{$minwidthdesktop}px){.adapt-img-wrapper.intrinsic-desktop::before{content:'';display:block;height:0;width:100%;}.adapt-img-wrapper.intrinsic-desktop img{position:absolute;left:0;top:0;width:100%;height:auto;}}"
347                                        .".adapt-img-background{width:100%;height:0}"
348                                        ."@media print{html .adapt-img-wrapper{background:none}"
349                                        ."</style>\n";
350                                        // JS that evaluate connection speed and add a aislow class on <html> if slow connection
351                                        // and onload JS that adds CSS to finish rendering
352                                        $async_style = "picture.adapt-img-wrapper{background-size:0;}";
353                                        $length = 1500; // ~1500 bytes for CSS and minified JS we add here
354                                        // minified version of AdaptiveImages-light.js (using https://closure-compiler.appspot.com/home)
355                                        $js = <<<JS
356(function(){function d(a){var b=document.documentElement;b.className=b.className+" "+a}function f(a){var b=window.onload;window.onload="function"!=typeof window.onload?a:function(){b&&b();a()}}document.createElement("picture");adaptImgLazy&&d("lazy");var a=!1;if("undefined"!==typeof window.performance)a=window.performance.timing,a=(a=~~(adaptImgDocLength/(a.responseEnd-a.connectStart)))&&50>a;else{var c=navigator.connection||navigator.mozConnection||navigator.webkitConnection;"undefined"!==typeof c&&
357(a=3==c.type||4==c.type||/^[23]g$/.test(c.type))}a&&d("aislow");var e=function(){var a=document.createElement("style");a.type="text/css";a.innerHTML=adaptImgAsyncStyles;var b=document.getElementsByTagName("style")[0];b.parentNode.insertBefore(a,b);window.matchMedia||window.onbeforeprint||beforePrint()};"undefined"!==typeof jQuery?jQuery(function(){jQuery(window).load(e)}):f(e)})();
358JS;
359                                        break;
360                                case '3layers':
361                                default:
362                                        $base_style = "<style type='text/css'>"."img.adapt-img,.lazy img.adapt-img{max-width:100%;height:auto;}img.adapt-img.blur{filter:blur(5px)}"
363                                        .".adapt-img-wrapper,.adapt-img-wrapper::after{display:block;max-width:100%;position:relative;-webkit-background-size:100% auto;-webkit-background-size:cover;background-size:cover;background-repeat:no-repeat;line-height:1px;overflow:hidden}"
364                                        .".adapt-img-background{width:100%;height:0}.adapt-img-background::after{display:none;width:100%;height:0;}"
365                                        ."html body .adapt-img-wrapper.lazy,html.lazy body .adapt-img-wrapper,html body .adapt-img-wrapper.lazy::after,html.lazy body .adapt-img-wrapper::after{background-image:none}"
366                                        .".adapt-img-wrapper::after{position:absolute;top:0;left:0;right:0;bottom:0;content:\"\"}"
367                                        ."@media print{html .adapt-img-wrapper{background:none}html .adapt-img-wrapper img {opacity:1}html .adapt-img-wrapper::after{display:none}}"
368                                        ."</style>\n";
369                                        // JS that evaluate connection speed and add a aislow class on <html> if slow connection
370                                        // and onload JS that adds CSS to finish rendering
371                                        $async_style = "html img.adapt-img{opacity:0.01}html .adapt-img-wrapper::after{display:none;}";
372                                        $length = 2000; // ~2000 bytes for CSS and minified JS we add here
373                                        // minified version of AdaptiveImages.js (using https://closure-compiler.appspot.com/home)
374                                        $js = <<<JS
375function adaptImgFix(d){var e=window.getComputedStyle(d.parentNode).backgroundImage.replace(/\W?\)$/,"").replace(/^url\(\W?|/,"");d.src=e&&"none"!=e?e:d.src}(function(){function d(a){var b=document.documentElement;b.className=b.className+" "+a}function e(a){var b=window.onload;window.onload="function"!=typeof window.onload?a:function(){b&&b();a()}}document.createElement("picture");adaptImgLazy&&d("lazy");/android 2[.]/i.test(navigator.userAgent.toLowerCase())&&d("android2");var c=!1;if("undefined"!==typeof window.performance)c=window.performance.timing,c=(c=~~(adaptImgDocLength/(c.responseEnd-c.connectStart)))&&50>c;else{var f=navigator.connection||navigator.mozConnection||navigator.webkitConnection;"undefined"!==typeof f&&(c=3==f.type||4==f.type||/^[23]g$/.test(f.type))}c&&d("aislow");var h=function(){var a=document.createElement("style");a.type="text/css";a.innerHTML=adaptImgAsyncStyles;var b=document.getElementsByTagName("style")[0];b.parentNode.insertBefore(a,b);window.matchMedia||window.onbeforeprint||g()};"undefined"!==typeof jQuery?jQuery(function(){jQuery(window).load(h)}):e(h);var g=function(){for(var a=document.getElementsByClassName("adapt-img"),b=0;b<a.length;b++)adaptImgFix(a[b])};window.matchMedia&&window.matchMedia("print").addListener(function(a){g()});"undefined"!==typeof window.onbeforeprint&&(window.onbeforeprint=g)})();
376JS;
377                                        // alternative noscript if no js (to de-activate progressive rendering on PNG and GIF)
378                                        if (!$this->nojsPngGifProgressiveRendering)
379                                                $noscript = "<noscript><style type='text/css'>.png img.adapt-img,.gif img.adapt-img{opacity:0.01} .adapt-img-wrapper.png::after,.adapt-img-wrapper.gif::after{display:none;}</style></noscript>";
380
381                                        break;
382                        }
383                        $length += strlen($html)+strlen($ins_style);
384                        $ins = "<script type='text/javascript'>/*<![CDATA[*/var adaptImgDocLength=$length;adaptImgAsyncStyles=\"$async_style\";adaptImgLazy=".($this->lazyload?"true":"false").";{$js}/*]]>*/</script>\n";
385                        $ins .= $noscript;
386                        $ins .= $ins_style;
387
388                        // insert before first <script or <link
389                        $ins = "$base_style<!--[if !IE]><!-->$ins\n<!--<![endif]-->\n";
390                        if ($p = strpos($html,"<link")
391                                or $p = strpos($html,"<script")
392                                or $p = strpos($html,"</head")
393                                or $p = strpos($html,"</body")) {
394                                $html = substr_replace($html,$ins,$p,0);
395                        }
396                        else {
397                                $html .= $ins;
398                        }
399                }
400                return $html;
401        }
402
403
404        /**
405         * Adapt each <img> from HTML part
406         *
407         * @param string $html
408         *   HTML source page
409         * @param int $maxWidth1x
410         *   max display width for images 1x
411         * @param bool $asBackgroundOrSizes
412         *   true : markup with image as a background only
413         *   string|array : sizes attribut for srcset method
414         * @param array|null $bkpt
415         * @return string
416         * @throws Exception
417         */
418        public function adaptHTMLPart($html,$maxWidth1x=null,$bkpt=null,$asBackgroundOrSizes=false){
419                $asBackground = false;
420                $sizes = null;
421                if ($asBackgroundOrSizes === true) {
422                        $asBackground = true;
423                }
424                elseif(is_array($asBackgroundOrSizes) or is_string($asBackgroundOrSizes)) {
425                        $sizes = $asBackgroundOrSizes;
426                }
427
428                static $bkpts = array();
429                if (is_null($maxWidth1x) OR !intval($maxWidth1x))
430                        $maxWidth1x = $this->maxWidth1x;
431
432                if (is_null($bkpt)){
433                        if ($maxWidth1x AND !isset($bkpts[$maxWidth1x])){
434                                $b = $this->defaultBkpts;
435                                while (count($b) AND end($b)>$maxWidth1x) array_pop($b);
436                                // la largeur maxi affichee
437                                if (!count($b) OR end($b)<$maxWidth1x) $b[] = $maxWidth1x;
438                                $bkpts[$maxWidth1x] = $b;
439                        }
440                        $bkpt = (isset($bkpts[$maxWidth1x])?$bkpts[$maxWidth1x]:null);
441                }
442                else {
443                        while (count($bkpt) AND end($bkpt)>$maxWidth1x) array_pop($bkpt);
444                }
445
446                $replace = array();
447                preg_match_all(",<img\s[^>]*>,Uims",$html,$matches,PREG_SET_ORDER);
448                if (count($matches)){
449                        foreach($matches as $m){
450                                $ri = $this->processImgTag($m[0], $bkpt, $maxWidth1x, $sizes, $asBackground);
451                                if ($ri!==$m[0]){
452                                        $replace[$m[0]] = $ri;
453                                }
454                        }
455                        if (count($replace)){
456                                $html = str_replace(array_keys($replace),array_values($replace),$html);
457                        }
458                }
459
460                return $html;
461        }
462
463
464
465        /**
466         * OnDemand production and delivery of BkptImage from it's URL
467         * @param string path
468         *   local/adapt-img/w/x/file
469         *   ex : 320/20x/file
470         *   w is the display width
471         *   x is the dpi resolution (10x => 1, 15x => 1.5, 20x => 2)
472         *   file is the original source image file path
473         * @throws Exception
474         */
475        public function deliverBkptImage($path){
476
477                try {
478                        $mime = "";
479                        $file = $this->processBkptImageFromPath($path, $mime);
480                }
481                catch (Exception $e){
482                        $file = "";
483                }
484                if (!$file
485                  OR !$mime){
486                        http_status(404);
487                        throw new InvalidArgumentException("Unable to find {$path} image");
488                }
489
490                header("Content-Type: ". $mime);
491                #header("Expires: 3600"); // set expiration time
492
493                if ($cl = filesize($file))
494                        header("Content-Length: ". $cl);
495
496                readfile($file);
497        }
498
499
500        /**
501         * Build an image variant for a resolution breakpoint
502         * file path of image is constructed from source file, width and resolution on scheme :
503         * bkptwidth/resolution/full/path/to/src/image/file
504         * it allows to reverse-build the image variant from the path
505         *
506         * if $force==false and $this->onDemandImages==true we only compute the file path
507         * and the image variant will be built on first request
508         *
509         * @param string $src
510         *   source image
511         * @param int $wkpt
512         *   breakpoint width (display width) for which the image is built
513         * @param int $wx
514         *   real width in px of image
515         * @param string $x
516         *   resolution 10x 15x 20x
517         * @param string $extension
518         *   extension
519         * @param bool $force
520         *   true to force immediate image building if not existing or if too old
521         * @param int $quality
522         *   to set an output image quality outside the predefined preset
523         * @return string
524         *   name of image file
525         * @throws Exception
526         */
527        protected function processBkptImage($src, $wkpt, $wx, $x, $extension, $force=false, $quality=null){
528                $dir_dest = $this->destDirectory."$wkpt/$x/";
529                $dest = $dir_dest . $this->adaptedSrcToURL($src);
530
531                if (($exist=file_exists($dest)) AND filemtime($dest)>=filemtime($src))
532                        return $dest;
533
534                $force = ($force?true:!$this->onDemandImages);
535
536                // if file already exists but too old, delete it if we don't want to generate it now
537                // it will be generated on first request
538                if ($exist AND !$force)
539                        @unlink($dest);
540
541                if (!$force)
542                        return $dest;
543
544                if (is_null($quality)){
545                        switch ($x) {
546                                case '10x':
547                                        $quality = $this->x10JpgQuality;
548                                        break;
549                                case '15x':
550                                        $quality = $this->x15JpgQuality;
551                                        break;
552                                case '20x':
553                                        $quality = $this->x20JpgQuality;
554                                        break;
555                        }
556                }
557
558                $i = $this->imgSharpResize($src,$dest,$wx,10000,$quality);
559                if ($i AND $i!==$dest AND $i!==$src){
560                        throw new Exception("Error in imgSharpResize: return \"$i\" whereas \"$dest\" expected");
561                }
562                if (!file_exists($i)){
563                        throw new Exception("Error file \"$i\" not found: check the right to write in ".$this->destDirectory);
564                }
565                return $i;
566        }
567
568
569        /**
570         * Build an image variant from it's URL
571         * this function is used when $this->onDemandImages==true
572         * needs a RewriteRule such as following and a router to call this function on first request
573         *
574         * RewriteRule \badapt-img/(\d+/\d\dx/.*)$ spip.php?action=adapt_img&arg=$1 [QSA,L]
575         *
576         * @param string $URLPath
577         * @param string $mime
578         * @return string
579         * @throws Exception
580         */
581        protected function processBkptImageFromPath($URLPath,&$mime){
582                $base = $this->destDirectory;
583                $path = $URLPath;
584                // if base path is provided, remove it
585                if (strncmp($path,$base,strlen($base))==0)
586                        $path = substr($path,strlen($base));
587
588                $path = explode("/",$path);
589                $wkpt = intval(array_shift($path));
590                $x = array_shift($path);
591                $url = implode("/",$path);
592
593                // translate URL part to file path
594                $src = $this->adaptedURLToSrc($url);
595
596                $parts = pathinfo($src);
597                $extension = strtolower($parts['extension']);
598                $mime = $this->extensionToMimeType($extension);
599                $dpi = array('10x'=>1,'15x'=>1.5,'20x'=>2);
600
601                // check that path is well formed
602                if (!$wkpt
603                  OR !isset($dpi[$x])
604                  OR !file_exists($src)
605                  OR !$mime){
606                        throw new Exception("Unable to build adapted image $URLPath");
607                }
608                $wx = intval(round($wkpt * $dpi[$x]));
609
610                $file = $this->processBkptImage($src, $wkpt, $wx, $x, $extension, true);
611                return $file;
612        }
613
614
615        /**
616         * Process one single <img> tag :
617         * extract informations of src attribute
618         * and data-src-mobile attribute if provided
619         * compute images versions for provided breakpoints
620         *
621         * Don't do anything if img width is lower than $this->minWidth1x
622         * or img filesize smaller than $this->minFileSize
623         *
624         * @param string $img
625         *   html img tag
626         * @param array $bkpt
627         *   breakpoints
628         * @param int $maxWidth1x
629         *   max display with of image (in 1x)
630         * @param string|array $sizes
631         *   informations for sizez attribut in the srcset method
632         * @param bool $asBackground
633         *   markup with image as a background only
634         * @return string
635         *   html markup : original markup or adapted markup
636         * @throws Exception
637         */
638        protected function processImgTag($img, $bkpt, $maxWidth1x, $sizes = null, $asBackground = false){
639                if (!$img) return $img;
640
641                // don't do anyting if has adapt-img (already adaptive) or no-adapt-img class (no adaptative needed)
642                if (strpos($img, "adapt-img")!==false)
643                        return $img;
644                if (is_null($bkpt) OR !is_array($bkpt))
645                        $bkpt = $this->defaultBkpts;
646
647                list($w,$h) = $this->imgSize($img);
648                $src = trim($this->tagAttribute($img, 'src'));
649                if (strlen($src)<1){
650                        $src = $img;
651                        $img = "<img src='".$src."' />";
652                }
653
654                $adapt = true;
655                // Don't do anything if img is to small or unknown width
656                if (!$w OR $w<=$this->minWidth1x) {
657                        $adapt = false;
658                }
659                else {
660                        $srcMobile = $this->tagAttribute($img, 'data-src-mobile');
661
662                        // don't do anything with data-URI images
663                        if (strncmp($src, "data:", 5)==0) {
664                                $adapt = false;
665                        }
666                        else {
667                                $src = $this->URL2filepath($src);
668                                // don't do anyting if we can't find file
669                                if (!$src or !file_exists($src)) {
670                                        $adapt = false;
671                                }
672                                else {
673                                        // Don't do anything if img filesize is to small
674                                        $filesize=@filesize($src);
675                                        if ($filesize AND $filesize<$this->minFileSize) {
676                                                $adapt = false;
677                                        }
678                                        else {
679                                                $parts = pathinfo($src);
680                                                $extension = $parts['extension'];
681                                                // don't do anyting if it's an animated GIF
682                                                if ($extension=="gif" AND $this->isAnimatedGif($src)) {
683                                                        $adapt = false;
684                                                }
685                                        }
686                                }
687                        }
688                }
689
690                if (!$adapt) {
691                        if (!$asBackground) {
692                                return $img;
693                        }
694
695                        $images[$w] = array(
696                                '10x' => $src,
697                        );
698                        // build the markup for background
699                        return $this->imgAdaptiveMarkup($img, $images, $w, $h, $extension, $maxWidth1x, $sizes, $asBackground);
700                }
701
702                if ($srcMobile) {
703                        $srcMobile = $this->URL2filepath($srcMobile);
704                        list($wmobile, $hmobile) = @getimagesize($srcMobile);
705                        if (!$wmobile) {
706                                $wmobile = $w;
707                        }
708                }
709
710                $images = array();
711                if ($w<end($bkpt))
712                        $images[$w] = array(
713                                '10x' => $src,
714                                '15x' => $src,
715                                '20x' => $src,
716                        );
717
718                // build images (or at least URLs of images) on breakpoints
719                $fallback = $src;
720                $wfallback = $w;
721                $dpi = array('10x' => 1, '15x' => 1.5, '20x' => 2);
722                $wk = 0;
723                foreach ($bkpt as $wk){
724                        if ($wk>$w) break;
725                        $is_mobile = (($srcMobile AND $wk<=$this->maxWidthMobileVersion) ? true : false);
726                        if ($is_mobile) {
727                                // say we have a mobile version for width under
728                                $images['maxWidthMobile'] = $this->maxWidthMobileVersion;
729                        }
730                        foreach ($dpi as $k => $x){
731                                $wkx = intval(round($wk*$x));
732                                if ($wkx>($is_mobile ? $wmobile: $w))
733                                        $images[$wk][$k] = $is_mobile ? $srcMobile : $src;
734                                else {
735                                        $images[$wk][$k] = $this->processBkptImage($is_mobile ? $srcMobile : $src, $wk, $wkx, $k, $extension);
736                                }
737                        }
738                        if ($wk<=$maxWidth1x
739                                AND ($wk<=$this->lowsrcWidth)
740                                AND ($is_mobile OR !$srcMobile)){
741                                $fallback = $images[$wk]['10x'];
742                                $wfallback = $wk;
743                        }
744                }
745
746                if (!$asBackground) {
747
748                        $fallback_directory = $this->destDirectory."fallback/";
749                        if (!is_null($this->thumbnailGeneratorCallback) && is_callable($this->thumbnailGeneratorCallback)) {
750                                $options = [
751                                        'dir' => $fallback_directory,
752                                        'images' => $images,
753                                        'src' => $src,
754                                        'srcMobile' => $srcMobile,
755                                        'lowsrcWidth' => $this->lowsrcWidth,
756                                        'lowsrcJpgQuality' => $this->lowsrcJpgQuality,
757                                ];
758                                $callback = $this->thumbnailGeneratorCallback;
759                                if ($res = $callback($img, $options)) {
760                                        list($image, $class) = $res;
761                                        $images["fallback"] = $image;
762                                        $images["fallback_class"] = $class;
763                                }
764                        }
765
766                        // default method for fallback generation if no external callback provided or if it failed
767                        if (empty($images["fallback"])) {
768
769                                // Build the fallback img : High-compressed JPG
770                                // Start from the mobile version if available or from the larger version otherwise
771                                if ($wk>$w
772                                        AND $w<$maxWidth1x
773                                        AND $w<$this->lowsrcWidth){
774                                        $fallback = $images[$w]['10x'];
775                                        $wfallback = $w;
776                                }
777
778                                $process_fallback = true;
779                                if ($wfallback > $this->lowsrcWidth) {
780
781                                        $bigger_mistake = $h;
782                                        $best_width = $this->lowsrcWidth;
783                                        // optimise this $wfallback to avoid a too big rounding mistake in the height thumbnail resizing
784                                        foreach ([1,1.25,1.333,1.5,1.666,1.75,2] as $x) {
785                                                $wfallback = round($x * $this->lowsrcWidth);
786                                                list($fw,$fh) = $this->computeImageSize($w, $h, $wfallback,10000);
787                                                $mistake = abs(($h - ($fh * $w / $fw)) * $maxWidth1x / $w);
788                                                if ($mistake < $bigger_mistake) {
789                                                        $best_width = $wfallback;
790                                                        $bigger_mistake = $mistake;
791                                                        // if less than 1px of rounding mistake, let's take this size
792                                                        if ($mistake < 1) {
793                                                                break;
794                                                        }
795                                                }
796                                        }
797                                        $wfallback = $best_width;
798
799
800                                        $q = $this->lowsrcQualityOptimize($wfallback, $this->lowsrcJpgQuality, $w, $h, $maxWidth1x);
801                                        $fallback = $this->processBkptImage($is_mobile ? $srcMobile : $src, $wfallback, $wfallback, '10x', $extension, true, $q);
802                                        // if it's already a jpg nothing more to do here, otherwise double compress produce artefacts
803                                        if ($extension === 'jpg') {
804                                                $process_fallback = false;
805                                        }
806                                }
807
808
809                                // if $this->onDemandImages == true image has not been built yet
810                                // in this case ask for immediate generation
811                                if (!file_exists($fallback)){
812                                        $mime = ""; // not used here
813                                        $this->processBkptImageFromPath($fallback, $mime);
814                                }
815
816                                if ($process_fallback) {
817                                        $q = $this->lowsrcQualityOptimize($wfallback, $this->lowsrcJpgQuality, $w, $h, $maxWidth1x);
818                                        $images["fallback"] = $this->img2JPG($fallback, $fallback_directory, $this->lowsrcJpgBgColor, $q);
819                                }
820                                else {
821                                        $infos = $this->readSourceImage($fallback, $fallback_directory, 'jpg');
822                                        if ($infos['creer']) {
823                                                @copy($fallback, $infos["fichier_dest"]);
824                                        }
825                                        $images["fallback"] =  $infos["fichier_dest"];
826                                }
827                                $images["fallback_class"] = 'blur';
828                        }
829                }
830
831                // limit $src image width to $maxWidth1x for old IE
832                $src = $this->processBkptImage($src,$maxWidth1x,$maxWidth1x,'10x',$extension,true);
833                list($w,$h) = $this->imgSize($src);
834                $img = $this->setTagAttribute($img,"src",$this->filepath2URL($src));
835                $img = $this->setTagAttribute($img,"width",$w);
836                $img = $this->setTagAttribute($img,"height",$h);
837
838                // ok, now build the markup
839                return $this->imgAdaptiveMarkup($img, $images, $w, $h, $extension, $maxWidth1x, $sizes, $asBackground);
840        }
841
842        /**
843         * Compute an "optimal" jpg quality for the fallback image
844         * @param $width_fallback
845         * @param $lowsrcBaseQuality
846         * @param $width
847         * @param $height
848         * @param $maxWidth1x
849         * @return float|mixed
850         */
851        protected function lowsrcQualityOptimize($width_fallback, $lowsrcBaseQuality, $width, $height, $maxWidth1x){
852                // $this->lowsrcJpgQuality give a base quality for a 450kpx image size
853                // quality is varying around this value (+/- 50%) depending of image pixel size
854                // in order to limit the weight of fallback (empirical rule)
855                $q = round($lowsrcBaseQuality-((min($maxWidth1x, $width_fallback)*$height/$width*min($maxWidth1x, $width_fallback))/75000-6));
856                $q = min($q, round($this->lowsrcJpgQuality)*1.5);
857                $q = max($q, round($this->lowsrcJpgQuality)*0.5);
858
859                return $q;
860        }
861
862        /**
863         * Build html markup with CSS rules in <style> tag
864         * from provided img tag an array of bkpt images
865         *
866         * @param string $img
867         *   source img tag
868         * @param array $bkptImages
869         *     falbback => file
870         *     width =>
871         *        10x => file
872         *        15x => file
873         *        20x => file
874         * @param int $width
875         * @param int $height
876         * @param string $extension
877         * @param int $maxWidth1x
878         * @param string|array $sizes
879         * @param bool $asBackground
880         * @return string
881         */
882        protected function imgAdaptiveMarkup($img, $bkptImages, $width, $height, $extension, $maxWidth1x, $sizes=null, $asBackground = false){
883                $class = $this->tagAttribute($img, "class");
884                if (strpos($class, "adapt-img")!==false){
885                        return $img;
886                }
887                ksort($bkptImages);
888
889                // provided fallback image?
890                $fallback_file = "";
891                $fallback_class = "";
892                if (isset($bkptImages['fallback'])){
893                        $fallback_file = $bkptImages['fallback'];
894                        unset($bkptImages['fallback']);
895                }
896                if (isset($bkptImages['fallback_class'])){
897                        $fallback_class = $bkptImages['fallback_class'];
898                        unset($bkptImages['fallback_class']);
899                }
900
901                // else we use the smallest one
902                if (!$fallback_file){
903                        $fallback_file = reset($bkptImages);
904                        $fallback_file = $fallback_file['10x'];
905                }
906
907                $maxWidthMobile = null;
908                if (isset($bkptImages['maxWidthMobile'])) {
909                        $maxWidthMobile = $bkptImages['maxWidthMobile'];
910                        unset($bkptImages['maxWidthMobile']);
911                }
912
913                if (!$asBackground and $this->markupMethod === 'srcset') {
914                        return $this->imgAdaptiveSrcsetMarkup($img, $fallback_file, $fallback_class, $bkptImages, $width, $height, $extension, $maxWidth1x, $maxWidthMobile, $sizes);
915                }
916
917                // default method
918                return $this->imgAdaptive3LayersMarkup($img, $fallback_file, $fallback_class, $bkptImages, $width, $height, $extension, $maxWidth1x, $asBackground);
919        }
920
921        /**
922         * Build html markup with CSS rules in <style> tag
923         * from provided img tag and array of bkpt images
924         *
925         * @param string $img
926         *   source img tag
927         * @param string $fallback_file
928         *   file for fallback/preview
929         * @param string $fallback_class
930         *   class for fallback/preview img
931         * @param array $bkptImages
932         *     falbback => file
933         *     width =>
934         *        10x => file
935         *        15x => file
936         *        20x => file
937         * @param int $width
938         * @param int $height
939         * @param string $extension
940         * @param int $maxWidth1x
941         * @param int $maxWidthMobile
942         * @param string|array $sizes
943         * @return string
944         */
945        protected function imgAdaptiveSrcsetMarkup($img, $fallback_file, $fallback_class, $bkptImages, $width, $height, $extension, $maxWidth1x, $maxWidthMobile=null, $sizes = null){
946                $originalClass = $class = $this->tagAttribute($img,"class");
947                $intrinsic = "";
948                if (strpos(" $class "," intrinsic ") !== false) {
949                        $intrinsic = " intrinsic";
950                }
951                elseif ($this->alwaysIntrinsic) {
952                        $intrinsic = " intrinsic";
953                        $class = trim("$class intrinsic");
954                }
955
956                $cid = "c".crc32(serialize($bkptImages));
957
958                // embed fallback as a DATA URI if not more than 32ko
959                $fallback_file = $this->base64EmbedFile($fallback_file);
960
961                $srcset = array();
962                if ($maxWidthMobile) {
963                        $srcset['mobile'] = array(
964                                '20x' => array(), // est-ce qu'on veut envoyer du mobile en 2x ?
965                                '15x' => array(), // est-ce qu'on veut envoyer du mobile en 1.5x ?
966                                '10x' => array(),
967                        );
968                }
969
970                $srcset['all'] = array(
971                        '20x' => array(),
972                        '15x' => array(),
973                        '10x' => array(),
974                );
975                $default_file = "";
976
977                foreach ($bkptImages as $w=>$files){
978                        foreach($files as $kx=>$file){
979                                $srcset_key = 'all';
980                                if ($maxWidthMobile and $w<=$maxWidthMobile) {
981                                        $srcset_key = 'mobile';
982                                }
983                                if (isset($srcset[$srcset_key][$kx])){
984                                        $url = $this->filepath2URL($file);
985                                        $ww = round($w * intval($kx) / 10);
986                                        $srcset[$srcset_key][$kx][] = "$url {$ww}w";
987                                }
988                        }
989                        // set the default src : will be the largest 1x file smaller than the max-width
990                        if (isset($files['10x']) and (!$default_file or $w <= $maxWidth1x)) {
991                                $default_file = $files['10x'];
992                        }
993                }
994
995                // Media-Queries
996                $style = "";
997                $originalStyle = $this->tagAttribute($img,"style");
998                if ($intrinsic){
999                        $ratio = round($height/$width * 100,2);
1000                        // if there is a mobile variation, set the intrinsic only in desktop version, cause the mobile image could have different ratio
1001                        // moreover, the source switch maybe not synchronized with the intrinsic ratio switch
1002                        if ($maxWidthMobile) {
1003                                $intrinsic = " intrinsic-desktop";
1004                                $minWidthDesktop = $maxWidthMobile + 0.5;
1005                                $style .= "@media (min-width: {$minWidthDesktop}px){.intrinsic-desktop.{$cid}::before {padding-bottom:{$ratio}%}}";
1006                        }
1007                        else {
1008                                $style .= ".intrinsic.{$cid}::before {padding-bottom:{$ratio}%}";
1009                        }
1010                }
1011
1012                $srcset_base = implode(', ', $srcset['all']['10x']);
1013                unset($srcset['all']['10x']);
1014
1015                // base sizes rule: fix the max width
1016                $sizes_rule = array();
1017                $default_size = false;
1018                $need_max_size = true;
1019                if (is_string($sizes)) {
1020                        $sizes = explode(',', $sizes);
1021                        $sizes = array_map('trim', $sizes);
1022                }
1023                if (is_array($sizes)) {
1024                        while(count($sizes)) {
1025                                $s = array_shift($sizes);
1026                                $sizes_rule[] = $s;
1027                                if (strpos($s, "(") === false and intval(trim($s))) {
1028                                        $default_size = intval(trim($s));
1029                                }
1030                                if (strpos($s, "min-width") !== false and strpos($s, "max-width") === false) {
1031                                        $need_max_size = false;
1032                                }
1033                        }
1034                }
1035                // set a defaut size rule if not provided (one without media query)
1036                if (!$default_size) {
1037                        $default_size = 100;
1038                        $sizes_rule[] = "{$default_size}vw";
1039                }
1040                if ($need_max_size) {
1041                        // set the max-size for super large screens
1042                        $maxScreenWidth = intval(ceil($maxWidth1x * 100 / $default_size));
1043                        array_unshift($sizes_rule, "(min-width: {$maxScreenWidth}px) {$maxWidth1x}px");
1044                }
1045
1046                $sizes_rule = implode(', ', $sizes_rule);
1047
1048                $sources = array();
1049                foreach ($srcset as $dest=>$srcset_dest) {
1050                        if ($dest === 'mobile') {
1051                                $mq_max_width = "(max-width:{$maxWidthMobile}px) and ";
1052                        }
1053                        else {
1054                                $mq_max_width = "";
1055                        }
1056                        foreach ($srcset_dest as $kx => $files) {
1057                                $files = implode(', ', $files);
1058                                $dp = intval($kx)/10;
1059                                $sources[] = "<source media=\"{$mq_max_width}(-webkit-min-device-pixel-ratio: {$dp}), {$mq_max_width}(min-resolution: {$dp}dppx)\" srcset=\"$files\" sizes=\"$sizes_rule\" >";
1060                        }
1061                }
1062                $sources = "<!--[if IE 9]><video style=\"display: none;\"><![endif]-->".implode("",$sources)."<!--[if IE 9]></video><![endif]-->";
1063
1064                $img = $this->setTagAttribute($img,"src",$this->filepath2URL($default_file));
1065                $img = $this->setTagAttribute($img,"class",trim("adapt-img $class"));
1066                if ($srcset_base) {
1067                        $img = $this->setTagAttribute($img,"srcset",$srcset_base);
1068                }
1069                $img = $this->setTagAttribute($img,"sizes",$sizes_rule);
1070
1071                // markup can be adjusted in hook, depending on style and class
1072                $markup = "<picture class=\"adapt-img-wrapper{$intrinsic} $cid $extension\" style=\"background-image:url($fallback_file)\">\n$sources\n$img</picture>";
1073                $markup = $this->imgMarkupHook($markup,$originalClass,$originalStyle);
1074
1075                if ($style) {
1076                        $markup .= "<!--[if !IE]><!--><style title='adaptive'>".$style."</style><!--<![endif]-->";
1077                }
1078                return $markup;
1079        }
1080
1081        /**
1082         * Build html markup with CSS rules in <style> tag
1083         * from provided img tag and array of bkpt images
1084         *
1085         * @param string $img
1086         *   source img tag
1087         * @param string $fallback_file
1088         *   file for fallback/preview
1089         * @param string $fallback_class
1090         *   class for fallback/preview img
1091         * @param array $bkptImages
1092         *     falbback => file
1093         *     width =>
1094         *        10x => file
1095         *        15x => file
1096         *        20x => file
1097         * @param int $width
1098         * @param int $height
1099         * @param string $extension
1100         * @param int $maxWidth1x
1101         * @param bool $asBackground
1102         * @return string
1103         */
1104        protected function imgAdaptive3LayersMarkup($img, $fallback_file, $fallback_class, $bkptImages, $width, $height, $extension, $maxWidth1x, $asBackground = false){
1105                $originalClass = $class = $this->tagAttribute($img,"class");
1106
1107                $cid = "c".crc32(serialize($bkptImages));
1108                $style = "";
1109                $img = $this->setTagAttribute($img,"class","adapt-img-ie $class");
1110                $class = trim("$fallback_class $class");
1111
1112                // embed fallback as a DATA URI if not more than 32ko
1113                $fallback_file = $this->base64EmbedFile($fallback_file);
1114                // if it is not a vectorial image, encapsulate it in a svg to avoid the pixel rounding flickering
1115                if (strpos($fallback_file, "image/svg") === false) {
1116                        $svg_wrapper = <<<SVG
1117<svg viewBox="0 0 $width $height" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><image width="$width" height="$height" xlink:href="$fallback_file" preserveAspectRatio="none"/></svg>
1118SVG;
1119                        $fallback_file = 'data:'.$this->extensionToMimeType('svg').';base64,'.base64_encode($svg_wrapper);
1120                }
1121
1122                $prev_width = 0;
1123                $medias = array();
1124                $lastw = array_keys($bkptImages);
1125                $lastw = end($lastw);
1126                $wandroid = 0;
1127                $islast = false;
1128                foreach ($bkptImages as $w=>$files){
1129                        if ($w==$lastw) {$islast = true;}
1130                        if ($w<=$this->maxWidthMobileVersion) $wandroid = $w;
1131                        // use min-width and max-width in order to avoid override
1132                        if ($prev_width<$maxWidth1x){
1133                                $hasmax = (($islast OR $w>=$maxWidth1x)?false:true);
1134                                $mw = ($prev_width?"and (min-width:{$prev_width}px)":"").($hasmax?" and (max-width:{$w}px)":"");
1135                                $htmlsel = "html:not(.android2)";
1136                                $htmlsel = array(
1137                                        '10x' => "$htmlsel",
1138                                        '15x' => "$htmlsel:not(.aislow)",
1139                                        '20x' => "$htmlsel:not(.aislow)",
1140                                );
1141                        }
1142                        $mwdpi = array(
1143                                '10x' => "screen $mw",
1144                                '15x' => "screen and (-webkit-min-device-pixel-ratio: 1.5) and (-webkit-max-device-pixel-ratio: 1.99) $mw,screen and (min--moz-device-pixel-ratio: 1.5) and (max--moz-device-pixel-ratio: 1.99) $mw",
1145                                '20x' => "screen and (-webkit-min-device-pixel-ratio: 2) $mw,screen and (min--moz-device-pixel-ratio: 2) $mw",
1146                        );
1147                        foreach($files as $kx=>$file){
1148                                if (isset($mwdpi[$kx])){
1149                                        $mw = $mwdpi[$kx];
1150                                        $not = $htmlsel[$kx];
1151                                        $url = $this->filepath2URL($file);
1152                                        $medias[$mw] = "@media $mw{{$not} .$cid,{$not} .$cid::after{background-image:url($url);}}";
1153                                }
1154                        }
1155                        $prev_width = $w+1;
1156                }
1157
1158                // One single CSS rule for old android browser (<3) which isn't able to manage override properly
1159                // we chose JPG 320px width - 1.5x as a compromise
1160                if ($wandroid){
1161                        $file = (isset($bkptImages[$wandroid]['15x']) ? $bkptImages[$wandroid]['15x'] : $bkptImages[$wandroid]['10x']);
1162                        $url = $this->filepath2URL($file);
1163                        $medias['android2'] = "html.android2 .$cid,html.android2 .$cid::after{background-image:url($url);}";
1164                }
1165
1166                // Media-Queries
1167                $style .= implode("",$medias);
1168                $originalStyle = $this->tagAttribute($img,"style");
1169
1170                if ($asBackground) {
1171                        // if we just want a background image: a span with a class
1172                        $ratio = round(100 * $height/$width, 2);
1173                        $out = "<span class=\"adapt-img-wrapper adapt-img-background $cid $extension\" style='padding-bottom: {$ratio}%;'></span>\n<style>".$style."</style>";
1174                }
1175                else {
1176                        $out = "<!--[if IE]>$img<![endif]-->\n";
1177
1178                        $img = $this->setTagAttribute($img,"src",$fallback_file);
1179                        $img = $this->setTagAttribute($img,"class",trim("adapt-img adapt-img-multilayers $class"));
1180                        $img = $this->setTagAttribute($img,"onmousedown","adaptImgFix(this)");
1181                        // $img = setTagAttribute($img,"onkeydown","adaptImgFix(this)"); // useful ?
1182
1183                        // markup can be adjusted in hook, depending on style and class
1184                        $markup = "<picture class=\"adapt-img-wrapper $cid $extension\">$img</picture>";
1185                        $markup = $this->imgMarkupHook($markup,$originalClass,$originalStyle);
1186
1187                        $out .= "<!--[if !IE]><!-->$markup\n<style title='adaptive'>".$style."</style><!--<![endif]-->";
1188                }
1189
1190                return $out;
1191        }
1192
1193
1194
1195        /**
1196         * Get height and width from an image file or <img> tag
1197         * use width and height attributes of provided <img> tag if possible
1198         * store getimagesize result in static to avoid multiple disk access if needed
1199         *
1200         * @param string $img
1201         * @return array
1202         *  (width,height)
1203         */
1204        protected function imgSize($img) {
1205
1206                static $largeur_img =array(), $hauteur_img= array();
1207                $srcWidth = 0;
1208                $srcHeight = 0;
1209
1210                $source = $this->tagAttribute($img,'src');
1211
1212                if (!$source) $source = $img;
1213                else {
1214                        $srcWidth = $this->tagAttribute($img,'width');
1215                        $srcHeight = $this->tagAttribute($img,'height');
1216                        if ($srcWidth AND $srcHeight)
1217                                return array($srcWidth,$srcHeight);
1218                        $source = $this->URL2filepath($source);
1219                }
1220
1221                // never process on remote img
1222                if (!$source OR preg_match(';^(\w{3,7}://);', $source)){
1223                        return array(0,0);
1224                }
1225
1226                if (isset($largeur_img[$source]))
1227                        $srcWidth = $largeur_img[$source];
1228                if (isset($hauteur_img[$source]))
1229                        $srcHeight = $hauteur_img[$source];
1230                if (!$srcWidth OR !$srcHeight){
1231                        if (file_exists($source)
1232                                AND $srcsize = @getimagesize($source)){
1233                                if (!$srcWidth) $largeur_img[$source] = $srcWidth = $srcsize[0];
1234                                if (!$srcHeight)        $hauteur_img[$source] = $srcHeight = $srcsize[1];
1235                        }
1236                }
1237                return array($srcWidth,$srcHeight);
1238        }
1239
1240
1241        /**
1242         * Find and get attribute value in an HTML tag
1243         * Regexp from function extraire_attribut() in
1244         * https://core.spip.net/projects/spip/repository/entry/spip/ecrire/inc/filtres.php#L2013
1245         * @param $tag
1246         *   html tag
1247         * @param $attribute
1248         *   attribute we look for
1249         * @param $full
1250         *   if true the function also returns the regexp match result
1251         * @return array|string
1252         */
1253        protected function tagAttribute($tag, $attribute, $full = false) {
1254                if (preg_match(
1255                ',(^.*?<(?:(?>\s*)(?>[\w:.-]+)(?>(?:=(?:"[^"]*"|\'[^\']*\'|[^\'"]\S*))?))*?)(\s+'
1256                .$attribute
1257                .'(?:=\s*("[^"]*"|\'[^\']*\'|[^\'"]\S*))?)()([^>]*>.*),isS',
1258
1259                $tag, $r)) {
1260                        if ($r[3][0] == '"' || $r[3][0] == "'") {
1261                                $r[4] = substr($r[3], 1, -1);
1262                                $r[3] = $r[3][0];
1263                        } elseif ($r[3]!=='') {
1264                                $r[4] = $r[3];
1265                                $r[3] = '';
1266                        } else {
1267                                $r[4] = trim($r[2]);
1268                        }
1269                        $att = str_replace("&#39;", "'", $r[4]);
1270                }
1271                else
1272                        $att = NULL;
1273
1274                if ($full)
1275                        return array($att, $r);
1276                else
1277                        return $att;
1278        }
1279
1280
1281        /**
1282         * change or insert an attribute of an html tag
1283         *
1284         * @param string $tag
1285         *   html tag
1286         * @param string $attribute
1287         *   attribute name
1288         * @param string $value
1289         *   new value
1290         * @param bool $protect
1291         *   protect value if true (remove newlines and convert quotes)
1292         * @param bool $removeEmpty
1293         *   if true remove attribute from html tag if empty
1294         * @return string
1295         *   modified tag
1296         */
1297        protected function setTagAttribute($tag, $attribute, $value, $protect=true, $removeEmpty=false) {
1298                // preparer l'attribut
1299                // supprimer les &nbsp; etc mais pas les balises html
1300                // qui ont un sens dans un attribut value d'un input
1301                if ($protect) {
1302                        $value = preg_replace(array(",\n,",",\s(?=\s),msS"),array(" ",""),strip_tags($value));
1303                        $value = str_replace(array("'",'"',"<",">"),array('&#039;','&#034;','&lt;','&gt;'), $value);
1304                }
1305
1306                // echapper les ' pour eviter tout bug
1307                $value = str_replace("'", "&#039;", $value);
1308                if ($removeEmpty AND strlen($value)==0)
1309                        $insert = '';
1310                else
1311                        $insert = " $attribute='$value'";
1312
1313                list($old, $r) = $this->tagAttribute($tag, $attribute, true);
1314
1315                if ($old !== NULL) {
1316                        // Remplacer l'ancien attribut du meme nom
1317                        $tag = $r[1].$insert.$r[5];
1318                }
1319                else {
1320                        // preferer une balise " />" (comme <img />)
1321                        if (preg_match(',/>,', $tag))
1322                                $tag = preg_replace(",\s?/>,S", $insert." />", $tag, 1);
1323                        // sinon une balise <a ...> ... </a>
1324                        else
1325                                $tag = preg_replace(",\s?>,S", $insert.">", $tag, 1);
1326                }
1327
1328                return $tag;
1329        }
1330
1331        /**
1332         * Provide Mime Type for Image file Extension
1333         * @param $extension
1334         * @return string
1335         */
1336        protected function extensionToMimeType($extension){
1337                static $MimeTable = array(
1338                        'jpg' => 'image/jpeg',
1339                        'jpeg' => 'image/jpeg',
1340                        'png' => 'image/png',
1341                        'gif' => 'image/gif',
1342                        'svg' => 'image/svg+xml',
1343                );
1344
1345                return (isset($MimeTable[$extension])?$MimeTable[$extension]:'image/jpeg');
1346        }
1347
1348
1349        /**
1350         * Detect animated GIF : don't touch it
1351         * https://www.php.net/manual/en/function.imagecreatefromgif.php#59787
1352         *
1353         * @param string $filename
1354         * @return bool
1355         */
1356        protected function isAnimatedGif($filename){
1357                $filecontents = file_get_contents($filename);
1358
1359                $str_loc = 0;
1360                $count = 0;
1361                while ($count<2) # There is no point in continuing after we find a 2nd frame
1362                {
1363
1364                        $where1 = strpos($filecontents, "\x00\x21\xF9\x04", $str_loc);
1365                        if ($where1===FALSE){
1366                                break;
1367                        } else {
1368                                $str_loc = $where1+1;
1369                                $where2 = strpos($filecontents, "\x00\x2C", $str_loc);
1370                                if ($where2===FALSE){
1371                                        break;
1372                                } else {
1373                                        if ($where1+8==$where2){
1374                                                $count++;
1375                                        }
1376                                        $str_loc = $where2+1;
1377                                }
1378                        }
1379                }
1380
1381                if ($count>1){
1382                        return (true);
1383
1384                } else {
1385                        return (false);
1386                }
1387        }
1388
1389        /**
1390         * Embed image file in Base 64 URI
1391         *
1392         * @param string $filename
1393         * @param int $maxsize
1394         * @return string
1395         *     URI Scheme of base64 if possible,
1396         *     or URL from source file
1397         */
1398        function base64EmbedFile ($filename, $maxsize = 32768) {
1399                $extension = substr(strrchr($filename,'.'),1);
1400
1401                if (!file_exists($filename)
1402                        OR filesize($filename)>$maxsize
1403                        OR !$content = file_get_contents($filename))
1404                        return $filename;
1405
1406                $base64 = base64_encode($content);
1407                $encoded = 'data:'.$this->extensionToMimeType($extension).';base64,'.$base64;
1408
1409                return $encoded;
1410        }
1411
1412
1413        /**
1414         * Convert image to JPG and replace transparency with a background color
1415         *
1416         * @param string $source
1417         *   source file name (or img tag)
1418         * @param string $destDir
1419         *   destination directory
1420         * @param string $bgColor
1421         *   hexa color
1422         * @param int $quality
1423         *   JPG quality
1424         * @return string
1425         *   file name of the resized image (or source image if fail)
1426         * @throws Exception
1427         */
1428        function img2JPG($source, $destDir, $bgColor='#000000', $quality=85) {
1429                $infos = $this->readSourceImage($source, $destDir, 'jpg');
1430
1431                if (!$infos) return $source;
1432
1433                $couleurs = $this->colorHEX2RGB($bgColor);
1434                $dr= $couleurs["red"];
1435                $dv= $couleurs["green"];
1436                $db= $couleurs["blue"];
1437
1438                $srcWidth = $infos["largeur"];
1439                $srcHeight = $infos["hauteur"];
1440
1441                if ($infos["creer"]) {
1442                        if ($this->maxImagePxGDMemoryLimit AND $srcWidth*$srcHeight>$this->maxImagePxGDMemoryLimit){
1443                                $this->log("No resize allowed : image is " . $srcWidth*$srcHeight . "px, larger than ".$this->maxImagePxGDMemoryLimit."px");
1444                                return $infos["fichier"];
1445                        }
1446                        $fonction_imagecreatefrom = $infos['fonction_imagecreatefrom'];
1447
1448                        if (!function_exists($fonction_imagecreatefrom))
1449                                return $infos["fichier"];
1450                        $im = @$fonction_imagecreatefrom($infos["fichier"]);
1451
1452                        if (!$im){
1453                                throw new Exception("GD image creation fail for ".$infos["fichier"]);
1454                        }
1455
1456                        $this->imagepalettetotruecolor($im);
1457                        $im_ = imagecreatetruecolor($srcWidth, $srcHeight);
1458                        if ($infos["format_source"] == "gif" AND function_exists('ImageCopyResampled')) {
1459                                // if was a transparent GIF
1460                                // make a tansparent PNG
1461                                @imagealphablending($im_, false);
1462                                @imagesavealpha($im_,true);
1463                                if (function_exists("imageAntiAlias")) imageAntiAlias($im_,true);
1464                                @ImageCopyResampled($im_, $im, 0, 0, 0, 0, $srcWidth, $srcHeight, $srcWidth, $srcHeight);
1465                                imagedestroy($im);
1466                                $im = $im_;
1467                        }
1468
1469                        // allocate background Color
1470                        $color_t = ImageColorAllocate( $im_, $dr, $dv, $db);
1471
1472                        imagefill ($im_, 0, 0, $color_t);
1473
1474                        // JPEG has no transparency layer, no need to copy
1475                        // the image pixel by pixel
1476                        if ($infos["format_source"] == "jpg") {
1477                                $im_ = &$im;
1478                        } else
1479                        for ($x = 0; $x < $srcWidth; $x++) {
1480                                for ($y=0; $y < $srcHeight; $y++) {
1481
1482                                        $rgb = ImageColorAt($im, $x, $y);
1483                                        $a = ($rgb >> 24) & 0xFF;
1484                                        $r = ($rgb >> 16) & 0xFF;
1485                                        $g = ($rgb >> 8) & 0xFF;
1486                                        $b = $rgb & 0xFF;
1487
1488                                        $a = (127-$a) / 127;
1489
1490                                        // faster if no transparency
1491                                        if ($a == 1) {
1492                                                $r = $r;
1493                                                $g = $g;
1494                                                $b = $b;
1495                                        }
1496                                        // faster if full transparency
1497                                        else if ($a == 0) {
1498                                                $r = $dr;
1499                                                $g = $dv;
1500                                                $b = $db;
1501
1502                                        }
1503                                        else {
1504                                                $r = round($a * $r + $dr * (1-$a));
1505                                                $g = round($a * $g + $dv * (1-$a));
1506                                                $b = round($a * $b + $db * (1-$a));
1507                                        }
1508                                        $a = (1-$a) *127;
1509                                        $color = ImageColorAllocateAlpha( $im_, $r, $g, $b, $a);
1510                                        imagesetpixel ($im_, $x, $y, $color);
1511                                }
1512                        }
1513                        if (!$this->saveGDImage($im_, $infos, $quality)){
1514                                throw new Exception("Unable to write ".$infos['fichier_dest'].", check write right of $destDir");
1515                        }
1516                        if ($im!==$im_)
1517                                imagedestroy($im);
1518                        imagedestroy($im_);
1519                }
1520                return $infos["fichier_dest"];
1521        }
1522
1523        /**
1524         * Resize without bluring, and save image with needed quality if JPG image
1525         * @author : Arno* from https://zone.spip.org/trac/spip-zone/browser/_plugins_/image_responsive/action/image_responsive.php
1526         *
1527         * @param string $source
1528         * @param string $dest
1529         * @param int $maxWidth
1530         * @param int $maxHeight
1531         * @param int|null $quality
1532         * @return string
1533         *   file name of the resized image (or source image if fail)
1534         * @throws Exception
1535         */
1536        function imgSharpResize($source, $dest, $maxWidth = 0, $maxHeight = 0, $quality=null){
1537                $infos = $this->readSourceImage($source, $dest);
1538                if (!$infos) return $source;
1539
1540                if ($maxWidth==0 AND $maxHeight==0)
1541                        return $source;
1542
1543                if ($maxWidth==0) $maxWidth = 10000;
1544                elseif ($maxHeight==0) $maxHeight = 10000;
1545
1546                $srcFile = $infos['fichier'];
1547                $srcExt = $infos['format_source'];
1548
1549                $destination = dirname($infos['fichier_dest']) . "/" . basename($infos['fichier_dest'], ".".$infos["format_dest"]);
1550
1551                // compute width & height
1552                $srcWidth = $infos['largeur'];
1553                $srcHeight = $infos['hauteur'];
1554                list($destWidth,$destHeight) = $this->computeImageSize($srcWidth, $srcHeight, $maxWidth, $maxHeight);
1555
1556                if ($infos['creer']==false)
1557                        return $infos['fichier_dest'];
1558
1559                // If source image is smaller than desired size, keep source
1560                if ($srcWidth
1561                  AND $srcWidth<=$destWidth
1562                  AND $srcHeight<=$destHeight){
1563
1564                        $infos['format_dest'] = $srcExt;
1565                        $infos['fichier_dest'] = $destination.".".$srcExt;
1566                        @copy($srcFile, $infos['fichier_dest']);
1567
1568                }
1569                else {
1570                        if ($this->maxImagePxGDMemoryLimit AND $srcWidth*$srcHeight>$this->maxImagePxGDMemoryLimit){
1571                                $this->log("No resize allowed : image is " . $srcWidth*$srcHeight . "px, larger than ".$this->maxImagePxGDMemoryLimit."px");
1572                                return $srcFile;
1573                        }
1574                        $destExt = $infos['format_dest'];
1575                        if (!$destExt){
1576                                throw new Exception("No output extension for {$srcFile}");
1577                        }
1578
1579                        $fonction_imagecreatefrom = $infos['fonction_imagecreatefrom'];
1580
1581                        if (!function_exists($fonction_imagecreatefrom))
1582                                return $srcFile;
1583                        $srcImage = @$fonction_imagecreatefrom($srcFile);
1584                        if (!$srcImage){
1585                                throw new Exception("GD image creation fail for {$srcFile}");
1586                        }
1587
1588                        // Initialization of dest image
1589                        $destImage = ImageCreateTrueColor($destWidth, $destHeight);
1590
1591                        // Copy and resize source image
1592                        $ok = false;
1593                        if (function_exists('ImageCopyResampled')){
1594                                // if transparent GIF, keep the transparency
1595                                if ($srcExt=="gif"){
1596                                        $transparent_index = ImageColorTransparent($srcImage);
1597                                        if($transparent_index!=(-1)){
1598                                                $transparent_color = ImageColorsForIndex($srcImage,$transparent_index);
1599                                                if(!empty($transparent_color)) {
1600                                                        $transparent_new = ImageColorAllocate($destImage,$transparent_color['red'],$transparent_color['green'],$transparent_color['blue']);
1601                                                        $transparent_new_index = ImageColorTransparent($destImage,$transparent_new);
1602                                                        ImageFill($destImage, 0,0, $transparent_new_index);
1603                                                }
1604                                        }
1605                                }
1606                                if ($destExt=="png"){
1607                                        // keep transparency
1608                                        if (function_exists("imageAntiAlias")) imageAntiAlias($destImage, true);
1609                                        @imagealphablending($destImage, false);
1610                                        @imagesavealpha($destImage, true);
1611                                }
1612                                $ok = @ImageCopyResampled($destImage, $srcImage, 0, 0, 0, 0, $destWidth, $destHeight, $srcWidth, $srcHeight);
1613                        }
1614                        if (!$ok)
1615                                $ok = ImageCopyResized($destImage, $srcImage, 0, 0, 0, 0, $destWidth, $destHeight, $srcWidth, $srcHeight);
1616
1617                        if ($destExt=="jpg" && function_exists('imageconvolution')){
1618                                $intSharpness = $this->computeSharpCoeff($srcWidth, $destWidth);
1619                                $arrMatrix = array(
1620                                        array(-1, -2, -1),
1621                                        array(-2, $intSharpness+12, -2),
1622                                        array(-1, -2, -1)
1623                                );
1624                                imageconvolution($destImage, $arrMatrix, $intSharpness, 0);
1625                        }
1626                        // save destination image
1627                        if (!$this->saveGDImage($destImage, $infos, $quality)){
1628                                throw new Exception("Unable to write ".$infos['fichier_dest'].", check write right of $dest");
1629                        }
1630
1631                        if ($srcImage)
1632                                ImageDestroy($srcImage);
1633                        ImageDestroy($destImage);
1634                }
1635
1636                return $infos['fichier_dest'];
1637
1638        }
1639
1640        /**
1641         * @author : Arno* from http:s//zone.spip.org/trac/spip-zone/browser/_plugins_/image_responsive/action/image_responsive.php
1642         *
1643         * @param int $intOrig
1644         * @param int $intFinal
1645         * @return mixed
1646         */
1647        function computeSharpCoeff($intOrig, $intFinal) {
1648          $intFinal = $intFinal * (750.0 / $intOrig);
1649          $intA     = 52;
1650          $intB     = -0.27810650887573124;
1651          $intC     = .00047337278106508946;
1652          $intRes   = $intA + $intB * $intFinal + $intC * $intFinal * $intFinal;
1653          return max(round($intRes), 0);
1654        }
1655
1656        /**
1657         * Read and preprocess informations about source image
1658         *
1659         * @param string $img
1660         *              HTML img tag <img src=... /> OR source filename
1661         * @param string $dest
1662         *              Destination dir of new image
1663         * @param null|string $outputFormat
1664         *              forced extension of output image file : jpg, png, gif
1665         * @return bool|array
1666         *              false in case of error
1667         *    array of image information otherwise
1668         * @throws Exception
1669         */
1670        protected function readSourceImage($img, $dest, $outputFormat = null) {
1671                if (strlen($img)==0) return false;
1672                $ret = array();
1673
1674                $source = trim($this->tagAttribute($img, 'src'));
1675                if (strlen($source) < 1){
1676                        $source = $img;
1677                        $img = "<img src='$source' />";
1678                }
1679                # gerer img src="data:....base64"
1680                # don't process base64
1681                else if (preg_match('@^data:image/(jpe?g|png|gif);base64,(.*)$@isS', $source)) {
1682                        return false;
1683                }
1684                else
1685                        $source = $this->URL2filepath($source);
1686
1687                // don't process distant images
1688                if (!$source OR preg_match(';^(\w{3,7}://);', $source)){
1689                        return false;
1690                }
1691
1692                $extension_dest = "";
1693                if (preg_match(",\.(gif|jpe?g|png)($|[?]),i", $source, $regs)) {
1694                        $extension = strtolower($regs[1]);
1695                        $extension_dest = $extension;
1696                }
1697                if (!is_null($outputFormat)) $extension_dest = $outputFormat;
1698
1699                if (!$extension_dest) return false;
1700
1701                if (@file_exists($source)){
1702                        list ($ret["largeur"],$ret["hauteur"]) = $this->imgSize(strpos($img,"width=")!==false?$img:$source);
1703                        $date_src = @filemtime($source);
1704                }
1705                else
1706                        return false;
1707
1708                // error if no known size
1709                if (!($ret["hauteur"] OR $ret["largeur"]))
1710                        return false;
1711
1712
1713                // dest filename : dest/md5(source) or dest if full name provided
1714                if (substr($dest,-1)=="/"){
1715                        $nom_fichier = md5($source);
1716                        $fichier_dest = $dest . $nom_fichier . "." . $extension_dest;
1717                }
1718                else
1719                        $fichier_dest = $dest;
1720
1721                $creer = true;
1722                if (@file_exists($f = $fichier_dest)){
1723                        if (filemtime($f)>=$date_src)
1724                                $creer = false;
1725                }
1726                // mkdir complete path if needed
1727                if ($creer
1728                  AND !is_dir($d=dirname($fichier_dest))){
1729                        mkdir($d,0777,true);
1730                        if (!is_dir($d)){
1731                                throw new Exception("Unable to mkdir {$d}");
1732                        }
1733                }
1734
1735                $ret["fonction_imagecreatefrom"] = "imagecreatefrom".($extension != 'jpg' ? $extension : 'jpeg');
1736                $ret["fichier"] = $source;
1737                $ret["fichier_dest"] = $fichier_dest;
1738                $ret["format_source"] = ($extension != 'jpeg' ? $extension : 'jpg');
1739                $ret["format_dest"] = $extension_dest;
1740                $ret["date_src"] = $date_src;
1741                $ret["creer"] = $creer;
1742                $ret["tag"] = $img;
1743
1744                if (!function_exists($ret["fonction_imagecreatefrom"])) return false;
1745                return $ret;
1746        }
1747
1748        /**
1749         * Compute new image size according to max Width and max Height and initial width/height ratio
1750         * @param int $srcWidth
1751         * @param int $srcHeight
1752         * @param int $maxWidth
1753         * @param int $maxHeight
1754         * @return array
1755         */
1756        function computeImageSize($srcWidth, $srcHeight, $maxWidth, $maxHeight) {
1757                $ratioWidth = $srcWidth/$maxWidth;
1758                $ratioHeight = $srcHeight/$maxHeight;
1759
1760                if ($ratioWidth <=1 AND $ratioHeight <=1) {
1761                        return array($srcWidth,$srcHeight);
1762                }
1763                else if ($ratioWidth < $ratioHeight) {
1764                        $destWidth = intval(round($srcWidth/$ratioHeight));
1765                        $destHeight = $maxHeight;
1766                }
1767                else {
1768                        $destWidth = $maxWidth;
1769                        $destHeight = intval(round($srcHeight/$ratioWidth));
1770                }
1771                return array ($destWidth, $destHeight);
1772        }
1773
1774        /**
1775         * SaveAffiche ou sauvegarde une image au format PNG
1776         * Utilise les fonctions specifiques GD.
1777         *
1778         * @param resource $img
1779         *   GD image resource
1780         * @param array $infos
1781         *   image description
1782         * @param int|null $quality
1783         *   compression quality for JPG images
1784         * @return bool
1785         */
1786        protected function saveGDImage($img, $infos, $quality=null) {
1787                $fichier = $infos['fichier_dest'];
1788                $tmp = $fichier.".tmp";
1789                switch($infos['format_dest']){
1790                        case "gif":
1791                                $ret = imagegif($img,$tmp);
1792                                break;
1793                        case "png":
1794                                $ret = imagepng($img,$tmp);
1795                                break;
1796                        case "jpg":
1797                        case "jpeg":
1798                                $ret = imagejpeg($img,$tmp,min($quality,100));
1799                                break;
1800                }
1801                if(file_exists($tmp)){
1802                        $taille_test = getimagesize($tmp);
1803                        if ($taille_test[0] < 1) return false;
1804
1805                        @unlink($fichier); // le fichier peut deja exister
1806                        @rename($tmp, $fichier);
1807                        return $ret;
1808                }
1809                return false;
1810        }
1811
1812
1813        /**
1814         * Convert indexed colors image to true color image
1815         * available in PHP 5.5+ https://www.php.net/manual/fr/function.imagepalettetotruecolor.php
1816         * @param resource $img
1817         * @return bool
1818         */
1819        protected function imagepalettetotruecolor(&$img) {
1820                if (function_exists("imagepalettetotruecolor"))
1821                        return imagepalettetotruecolor($img);
1822
1823                if ($img AND !imageistruecolor($img) AND function_exists('imagecreatetruecolor')) {
1824                        $w = imagesx($img);
1825                        $h = imagesy($img);
1826                        $img1 = imagecreatetruecolor($w,$h);
1827                        // keep alpha layer if possible
1828                        if(function_exists('ImageCopyResampled')) {
1829                                if (function_exists("imageAntiAlias")) imageAntiAlias($img1,true);
1830                                @imagealphablending($img1, false);
1831                                @imagesavealpha($img1,true);
1832                                @ImageCopyResampled($img1, $img, 0, 0, 0, 0, $w, $h, $w, $h);
1833                        } else {
1834                                imagecopy($img1,$img,0,0,0,0,$w,$h);
1835                        }
1836
1837                        $img = $img1;
1838                        return true;
1839                }
1840                return false;
1841        }
1842
1843
1844        /**
1845         * Translate HTML color to hexa color
1846         * @param string $color
1847         * @return string
1848         */
1849        protected function colorHTML2Hex($color){
1850                static $html_colors=array(
1851                        'aqua'=>'00FFFF','black'=>'000000','blue'=>'0000FF','fuchsia'=>'FF00FF','gray'=>'808080','green'=>'008000','lime'=>'00FF00','maroon'=>'800000',
1852                        'navy'=>'000080','olive'=>'808000','purple'=>'800080','red'=>'FF0000','silver'=>'C0C0C0','teal'=>'008080','white'=>'FFFFFF','yellow'=>'FFFF00');
1853                if (isset($html_colors[$lc=strtolower($color)]))
1854                        return $html_colors[$lc];
1855                return $color;
1856        }
1857
1858        /**
1859         * Translate hexa color to RGB
1860         * @param string $color
1861         *   hexa color (#000000 to #FFFFFF).
1862         * @return array
1863         */
1864        protected function colorHEX2RGB($color) {
1865                $color = $this->colorHTML2Hex($color);
1866                $color = ltrim($color,"#");
1867                $retour["red"] = hexdec(substr($color, 0, 2));
1868                $retour["green"] = hexdec(substr($color, 2, 2));
1869                $retour["blue"] = hexdec(substr($color, 4, 2));
1870
1871                return $retour;
1872        }
1873
1874}
Note: See TracBrowser for help on using the repository browser.