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

Last change on this file since 79116 was 79116, checked in by cedric@…, 6 years ago

On externalise tout le code Adaptive Images dans une librairie agnostique qui fonctionne de facon autonome fournie par https://github.com/nursit/AdaptiveImages
le plugin se contente de brancher les pipelines SPIP sur les entrees de la librairie

File size: 38.0 KB
Line 
1<?php
2/**
3 * AdaptiveImages
4 *
5 * @version    1.0.0
6 * @copyright  2013
7 * @author     Nursit
8 * @licence    GNU/GPL3
9 * @source     https://github.com/nursit/AdaptiveImages
10 */
11
12
13class AdaptiveImages {
14        /**
15         * @var AdaptiveImages
16         */
17        static protected $instance;
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 = 10;
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(160,320,480,640,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 = 320;
74
75        /**
76         * Maximum width for delivering mobile version in data-src-mobile=""
77         * @var int
78         */
79        protected $maxWidthMobileVersion = 320;
80
81        /**
82         * Set to true to generate adapted image only at first request from users
83         * (speed up initial page generation)
84         * @var int
85         */
86        protected $onDemandImages = false;
87
88
89        /**
90         * Allowed format images to be adapted
91         * @var array
92         */
93        protected $acceptedFormats = array('gif','png','jpeg','jpg');
94
95        /**
96         * directory for storing adaptive images
97         * @var string
98         */
99        protected $destDirectory = "local/adapt-img/";
100
101
102        /**
103         * Constructor
104         */
105        protected function __construct(){
106        }
107
108        /**
109         * get
110         * @param $property
111         * @return mixed
112         * @throws InvalidArgumentException
113         */
114        public function __get($property){
115                if(!property_exists($this,$property) OR $property=="instance") {
116      throw new InvalidArgumentException("Property {$property} doesn't exist");
117    }
118                return $this->{$property};
119        }
120
121        /**
122         * set
123         * @param $property
124         * @param $value
125         * @return mixed
126         * @throws InvalidArgumentException
127         */
128        public function __set($property, $value){
129                if(!property_exists($this,$property) OR $property=="instance") {
130      throw new InvalidArgumentException("Property {$property} doesn't exist");
131    }
132                if (in_array($property,array("nojsPngGifProgressiveRendering","onDemandImages"))){
133                        if (!is_bool($value))
134                                throw new InvalidArgumentException("Property {$property} needs a bool value");
135                }
136                elseif (in_array($property,array("lowsrcJpgBgColor","destDirectory"))){
137                        if (!is_string($value))
138                                throw new InvalidArgumentException("Property {$property} needs a string value");
139                }
140                elseif (in_array($property,array("defaultBkpts","acceptedFormats"))){
141                        if (!is_array($value))
142                                throw new InvalidArgumentException("Property {$property} needs an array value");
143                }
144                elseif (!is_int($value)){
145                        throw new InvalidArgumentException("Property {$property} needs an int value");
146                }
147                if ($property=="defaultBkpts"){
148                        sort($value);
149                }
150
151                return ($this->{$property} = $value);
152        }
153
154        /**
155         * Disable cloning
156         */
157        protected function __clone() {
158         trigger_error("Cannot clone a singleton class", E_USER_ERROR);
159        }
160
161        /**
162         * Retrieve the AdaptiveImages object
163         *
164         * @return AdaptiveImages
165         */
166        static public function getInstance() {
167         if (!(self::$instance instanceof self)) {
168           self::$instance = new self;
169         }
170         return self::$instance;
171        }
172
173
174        /**
175         * Process the full HTML page :
176         *  - adapt all <img> in the HTML
177         *  - collect all inline <style> and put in the <head>
178         *  - add necessary JS
179         *
180         * @param string $html
181         *   HTML source page
182         * @param int $maxWidth1x
183         *   max display width for images 1x
184         * @return string
185         *  HTML modified page
186         */
187        public function adaptHTMLPage($html,$maxWidth1x=null){
188                // adapt all images that need it, if not already
189                $html = $this->adaptHTMLPart($html, $maxWidth1x);
190
191                // if there is adapted images in the page, add the necessary CSS and JS
192                if (strpos($html,"adapt-img-wrapper")!==false){
193                        // Common styles for all adaptive images during loading
194                        $ins = "<style type='text/css'>"."img.adapt-img{opacity:0.70;max-width:100%;height:auto;}"
195                        ."span.adapt-img-wrapper,span.adapt-img-wrapper:after{display:inline-block;max-width:100%;position:relative;-webkit-background-size:100% auto;background-size:100% auto;background-repeat:no-repeat;line-height:1px;}"
196                        ."span.adapt-img-wrapper:after{position:absolute;top:0;left:0;right:0;bottom:0;content:\"\"}"
197                        ."</style>\n";
198                        // JS that evaluate connection speed and add a aislow class on <html> if slow connection
199                        // and onload JS that adds CSS to finish rendering
200                        $async_style = "html img.adapt-img{opacity:0.01}html span.adapt-img-wrapper:after{display:none;}";
201                        $length = strlen($html)+2000; // ~2000 pour le JS qu'on va inserer
202                        $ins .= "<script type='text/javascript'>/*<![CDATA[*/"
203                                ."function adaptImgFix(n){var i=window.getComputedStyle(n.parentNode).backgroundImage.replace(/\W?\)$/,'').replace(/^url\(\W?|/,'');n.src=(i&&i!='none'?i:n.src);}"
204                                ."(function(){function hAC(c){(function(H){H.className=H.className+' '+c})(document.documentElement)}"
205                                // Android 2 media-queries bad support workaround
206                                // muliple rules = multiples downloads : put .android2 on <html>
207                                // use with simple css without media-queries and send compressive image
208                                ."var android2 = (/android 2[.]/i.test(navigator.userAgent.toLowerCase()));"
209                                ."if (android2) {hAC('android2');}\n"
210                                // slowConnection detection
211                                ."var slowConnection = false;"
212                                ."if (typeof window.performance!==\"undefined\"){"
213                                ."var perfData = window.performance.timing;"
214                                ."var speed = ~~($length/(perfData.responseEnd - perfData.connectStart));" // approx, *1000/1024 to be exact
215                                //."console.log(speed);"
216                                ."slowConnection = (speed && speed<50);" // speed n'est pas seulement une bande passante car prend en compte la latence de connexion initiale
217                                ."}else{"
218                                //https://github.com/Modernizr/Modernizr/blob/master/feature-detects/network/connection.js
219                                ."var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;"
220                                ."if (typeof connection!==\"undefined\") slowConnection = (connection.type == 3 || connection.type == 4 || /^[23]g$/.test(connection.type));"
221                                ."}"
222                                //."console.log(slowConnection);"
223                                ."if(slowConnection) {hAC('aislow');}\n"
224                                // injecter async style async after images are loaded
225                          // in order to hide 2 top layers and show only lower one
226                                ."var adaptImg_onload = function(){"
227                          ."var sa = document.createElement('style'); sa.type = 'text/css';"
228                          ."sa.innerHTML = '$async_style';"
229                          ."var s = document.getElementsByTagName('style')[0]; s.parentNode.insertBefore(sa, s);};"
230                                // http://www.webreference.com/programming/javascript/onloads/index.html
231                                ."function addLoadEvent(func){var oldol=window.onload;if (typeof oldol != 'function'){window.onload=func;}else{window.onload=function(){if (oldol){oldol();} func();}}}"
232                                ."if (typeof jQuery!=='undefined') jQuery(function(){jQuery(window).load(adaptImg_onload)}); else addLoadEvent(adaptImg_onload);"
233                          ."})();/*]]>*/</script>\n";
234                        // alternative noscript if no js (to de-activate progressive rendering on PNG and GIF)
235                        if (!$this->nojsPngGifProgressiveRendering)
236                                $ins .= "<noscript><style type='text/css'>.png img.adapt-img,.gif img.adapt-img{opacity:0.01}span.adapt-img-wrapper.png:after,span.adapt-img-wrapper.gif:after{display:none;}</style></noscript>";
237
238                        // collect all adapt-img <style> in order to put it in the <head>
239                        preg_match_all(",<!--\[if !IE\]><!-->.*(<style[^>]*>.*</style>).*<!--<!\[endif\]-->,Ums",$html,$matches);
240                        if (count($matches[1])){
241                                $html = str_replace($matches[1],"",$html);
242                                $ins .= implode("\n",$matches[1]);
243                        }
244                        // insert before first <script or <link
245                        if ($p = strpos($html,"<link") OR $p = strpos($html,"<script") OR $p = strpos($html,"</head"))
246                                $html = substr_replace($html,"<!--[if !IE]-->$ins\n<!--[endif]-->\n",$p,0);
247                }
248                return $html;
249        }
250
251
252        /**
253         * Adapt each <img> from HTML part
254         *
255         * @param string $html
256         *   HTML source page
257         * @param int $maxWidth1x
258         *   max display width for images 1x
259         * @return string
260         */
261        public function adaptHTMLPart($html,$maxWidth1x=null){
262                static $bkpts = array();
263                if (is_null($maxWidth1x) OR !intval($maxWidth1x))
264                        $maxWidth1x = $this->maxWidth1x;
265
266                if ($maxWidth1x AND !isset($bkpts[$maxWidth1x])){
267                        $b = $this->defaultBkpts;
268                        while (count($b) AND end($b)>$maxWidth1x) array_pop($b);
269                        // la largeur maxi affichee
270                        if (!count($b) OR end($b)<$maxWidth1x) $b[] = $maxWidth1x;
271                        $bkpts[$maxWidth1x] = $b;
272                }
273                $bkpt = (isset($bkpts[$maxWidth1x])?$bkpts[$maxWidth1x]:null);
274
275                $replace = array();
276                preg_match_all(",<img\s[^>]*>,Uims",$html,$matches,PREG_SET_ORDER);
277                if (count($matches)){
278                        foreach($matches as $m){
279                                $ri = $this->processImgTag($m[0], $bkpt, $maxWidth1x);
280                                if ($ri!==$m[0]){
281                                        $replace[$m[0]] = $ri;
282                                }
283                        }
284                        if (count($replace)){
285                                $html = str_replace(array_keys($replace),array_values($replace),$html);
286                        }
287                }
288
289                return $html;
290        }
291
292
293
294        /**
295         * OnDemand production and delivery of BkptImage from it's URL
296         * @param string path
297         *   local/adapt-img/w/x/file
298         *   ex : 320/20x/file
299         *   w is the display width
300         *   x is the dpi resolution (10x => 1, 15x => 1.5, 20x => 2)
301         *   file is the original source image file path
302         * @throws Exception
303         */
304        public function deliverBkptImage($path){
305
306                try {
307                        $file = $this->processBkptImageFromPath($path, $mime);
308                }
309                catch (Exception $e){
310                        $file = "";
311                }
312                if (!$file
313                  OR !$mime){
314                        http_status(404);
315                        throw new InvalidArgumentException("Unable to find {$path} image");
316                }
317
318                header("Content-Type: ". $mime);
319                #header("Expires: 3600"); // set expiration time
320
321                if ($cl = filesize($file))
322                        header("Content-Length: ". $cl);
323
324                readfile($file);
325        }
326
327
328        /**
329         * Build an image variant for a resolution breakpoint
330         * file path of image is constructed from source file, width and resolution on scheme :
331         * bkptwidth/resolution/full/path/to/src/image/file
332         * it allows to reverse-build the image variant from the path
333         *
334         * if $force==false and $this->onDemandImages==true we only compute the file path
335         * and the image variant will be built on first request
336         *
337         * @param string $src
338         *   source image
339         * @param int $wkpt
340         *   breakpoint width (display width) for which the image is built
341         * @param int $wx
342         *   real width in px of image
343         * @param string $x
344         *   resolution 10x 15x 20x
345         * @param string $extension
346         *   extension
347         * @param bool $force
348         *   true to force immediate image building if not existing or if too old
349         * @return string
350         *   name of image file
351         * @throws Exception
352         */
353        protected function processBkptImage($src, $wkpt, $wx, $x, $extension, $force=false){
354                $dir_dest = $this->destDirectory."$wkpt/$x/";
355                $dest = $dir_dest.$src;
356
357                if (($exist=file_exists($dest)) AND filemtime($dest)>=filemtime($src))
358                        return $dest;
359
360                $force = ($force?true:!$this->onDemandImages);
361
362                // if file already exists but too old, delete it if we don't want to generate it now
363                // it will be generated on first request
364                if ($exist AND !$force)
365                        @unlink($dest);
366
367                if (!$force)
368                        return $dest;
369
370                switch($x){
371                        case '10x':
372                                $quality = $this->x10JpgQuality;
373                                break;
374                        case '15x':
375                                $quality = $this->x15JpgQuality;
376                                break;
377                        case '20x':
378                                $quality = $this->x20JpgQuality;
379                                break;
380                }
381
382                $i = $this->imgSharpResize($src,$dir_dest,$wx,10000,$quality);
383                if ($i AND $i!==$dest AND $i!==$src AND $i!==preg_replace(",\.gif$,",".png",$dest)){
384                        throw new Exception("Error in imgSharpResize : return \"$i\" whereas \"$dest\" expected");
385                }
386                return $i;
387        }
388
389
390        /**
391         * Build an image variant from it's URL
392         * this function is used when $this->onDemandImages==true
393         * needs a RewriteRule such as following and a router to call this function on first request
394         *
395         * RewriteRule \badapt-img/(\d+/\d\dx/.*)$ spip.php?action=adapt_img&arg=$1 [QSA,L]
396         *
397         * @param string $URLPath
398         * @param string $mime
399         * @return string
400         * @throws Exception
401         */
402        protected function processBkptImageFromPath($URLPath,&$mime){
403                $base = $this->destDirectory;
404                $path = $URLPath;
405                // if base path is provided, remove it
406                if (strncmp($path,$base,strlen($base))==0)
407                        $path = substr($path,strlen($base));
408
409                $path = explode("/",$path);
410                $wkpt = intval(array_shift($path));
411                $x = array_shift($path);
412                $src = implode("/",$path);
413
414                $parts = pathinfo($src);
415                $extension = strtolower($parts['extension']);
416                $mime = $this->extensionToMimeType($extension);
417                $dpi = array('10x'=>1,'15x'=>1.5,'20x'=>2);
418
419                // check that path is well formed
420                if (!$wkpt
421                  OR !isset($dpi[$x])
422                  OR !file_exists($src)
423                  OR !$mime){
424                        throw new Exception("Unable to build adapted image $URLPath");
425                }
426                $wx = intval(round($wkpt * $dpi[$x]));
427
428                $file = $this->processBkptImage($src, $wkpt, $wx, $x, $extension, true);
429                return $file;
430        }
431
432
433        /**
434         * Process one single <img> tag :
435         * extract informations of src attribute
436         * and data-src-mobile attribute if provided
437         * compute images versions for provided breakpoints
438         *
439         * Don't do anything if img width is lower than $this->minWidth1x
440         *
441         * @param string $img
442         *   html img tag
443         * @param array $bkpt
444         *   breakpoints
445         * @param int $maxWidth1x
446         *   max display with of image (in 1x)
447         * @return string
448         *   html markup : original markup or adapted markup
449         */
450        protected function processImgTag($img, $bkpt, $maxWidth1x){
451                if (!$img) return $img;
452
453                // don't do anyting if has adapt-img (already adaptive) or no-adapt-img class (no adaptative needed)
454                if (strpos($img, "adapt-img")!==false)
455                        return $img;
456                if (is_null($bkpt) OR !is_array($bkpt))
457                        $bkpt = $this->defaultBkpts;
458
459                list($w,$h) = $this->imgSize($img);
460                // Don't do anything if img is to small or unknown width
461                if (!$w OR $w<=$this->minWidth1x) return $img;
462
463                $src = trim($this->tagAttribute($img, 'src'));
464                if (strlen($src)<1){
465                        $src = $img;
466                        $img = "<img src='".$src."' />";
467                }
468                $srcMobile = $this->tagAttribute($img, 'data-src-mobile');
469
470                // don't do anyting of data-URI images
471                if (strncmp($src, "data:", 5)==0)
472                        return $img;
473
474                $images = array();
475                if ($w<end($bkpt))
476                        $images[$w] = array(
477                                '10x' => $src,
478                                '15x' => $src,
479                                '20x' => $src,
480                        );
481                $src = preg_replace(',[?][0-9]+$,', '', $src);
482
483                // don't do anyting if we can't find file
484                if (!file_exists($src))
485                        return $img;
486
487                $parts = pathinfo($src);
488                $extension = $parts['extension'];
489
490                // don't do anyting if it's an animated GIF
491                if ($extension=="gif" AND $this->isAnimatedGif($src))
492                        return $img;
493
494                // build images (or at least URLs of images) on breakpoints
495                $fallback = $src;
496                $wfallback = $w;
497                $dpi = array('10x' => 1, '15x' => 1.5, '20x' => 2);
498                $wk = 0;
499                foreach ($bkpt as $wk){
500                        if ($wk>$w) break;
501                        $is_mobile = (($srcMobile AND $wk<=$this->maxWidthMobileVersion) ? true : false);
502                        foreach ($dpi as $k => $x){
503                                $wkx = intval(round($wk*$x));
504                                if ($wkx>$w)
505                                        $images[$wk][$k] = $src;
506                                else {
507                                        $images[$wk][$k] = $this->processBkptImage($is_mobile ? $srcMobile : $src, $wk, $wkx, $k, $extension);
508                                }
509                        }
510                        if ($wk<=$maxWidth1x AND ($is_mobile OR !$srcMobile)){
511                                $fallback = $images[$wk]['10x'];
512                                $wfallback = $wk;
513                        }
514                }
515
516                // Build the fallback img : High-compressed JPG
517                // Start from the mobile version if available or from the larger version otherwise
518                if ($wk>$w && $w<$maxWidth1x){
519                        $fallback = $images[$w]['10x'];
520                        $wfallback = $w;
521                }
522
523
524                // if $this->onDemandImages == true image has not been built yet
525                // in this case ask for immediate generation
526                if (!file_exists($fallback)){
527                        $mime = ""; // not used here
528                        $this->processBkptImageFromPath($fallback, $mime);
529                }
530
531                // $this->lowsrcJpgQuality give a base quality for a 450kpx image size
532                // quality is varying around this value (+/- 50%) depending of image pixel size
533                // in order to limit the weight of fallback (empirical rule)
534                $q = round($this->lowsrcJpgQuality-((min($maxWidth1x, $wfallback)*$h/$w*min($maxWidth1x, $wfallback))/75000-6));
535                $q = min($q, round($this->lowsrcJpgQuality)*1.5);
536                $q = max($q, round($this->lowsrcJpgQuality)*0.5);
537                $images["fallback"] = $this->img2JPG($fallback, $this->destDirectory."fallback/", $this->lowsrcJpgBgColor, $q);
538
539                // limit $src image width to $maxWidth1x for old IE
540                $src = $this->processBkptImage($src,$maxWidth1x,$maxWidth1x,'10x',$extension,true);
541                list($w,$h) = $this->imgSize($src);
542                $img = $this->setTagAttribute($img,"src",$src);
543                $img = $this->setTagAttribute($img,"width",$w);
544                $img = $this->setTagAttribute($img,"height",$h);
545
546                // ok, now build the markup
547                return $this->imgAdaptiveMarkup($img, $images, $w, $h, $extension, $maxWidth1x);
548        }
549
550
551        /**
552         * Build html markup with CSS rules in <style> tag
553         * from provided img tag an array of bkpt images
554         *
555         * @param string $img
556         *   source img tag
557         * @param array $bkptImages
558         *     falbback => file
559         *     width =>
560         *        10x => file
561         *        15x => file
562         *        20x => file
563         * @param int $width
564         * @param int $height
565         * @param string $extension
566         * @param int $maxWidth1x
567         * @return string
568         */
569        function imgAdaptiveMarkup($img, $bkptImages, $width, $height, $extension, $maxWidth1x){
570                $class = $this->tagAttribute($img,"class");
571                if (strpos($class,"adapt-img")!==false) return $img;
572                ksort($bkptImages);
573                $cid = "c".crc32(serialize($bkptImages));
574                $style = "";
575                if ($class) $class = " $class";
576                $class = "$cid$class";
577                $img = $this->setTagAttribute($img,"class","adapt-img-ie $class");
578
579                // provided fallback image?
580                $fallback_file = "";
581                if (isset($bkptImages['fallback'])){
582                        $fallback_file = $bkptImages['fallback'];
583                        unset($bkptImages['fallback']);
584                }
585                // else we use the smallest one
586                if (!$fallback_file){
587                        $fallback_file = reset($bkptImages);
588                        $fallback_file = $fallback_file['10x'];
589                }
590                // embed fallback as a DATA URI if not more than 32ko
591                $fallback_file = $this->base64EmbedFile($fallback_file);
592
593                $prev_width = 0;
594                $medias = array();
595                $lastw = array_keys($bkptImages);
596                $lastw = end($lastw);
597                $wandroid = 0;
598                foreach ($bkptImages as $w=>$files){
599                        if ($w==$lastw) {$islast = true;}
600                        if ($w<=$this->maxWidthMobileVersion) $wandroid = $w;
601                        // use min-width and max-width in order to avoid override
602                        if ($prev_width<$maxWidth1x){
603                                $hasmax = (($islast OR $w>=$maxWidth1x)?false:true);
604                                $mw = ($prev_width?"and (min-width:{$prev_width}px)":"").($hasmax?" and (max-width:{$w}px)":"");
605                                $htmlsel = "html:not(.android2)";
606                                $htmlsel = array(
607                                        '10x' => "$htmlsel",
608                                        '15x' => "$htmlsel:not(.aislow)",
609                                        '20x' => "$htmlsel:not(.aislow)",
610                                );
611                        }
612                        $mwdpi = array(
613                                '10x' => "screen $mw",
614                                '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",
615                                '20x' => "screen and (-webkit-min-device-pixel-ratio: 2) $mw,screen and (min--moz-device-pixel-ratio: 2) $mw",
616                        );
617                        foreach($files as $kx=>$file){
618                                if (isset($mwdpi[$kx])){
619                                        $mw = $mwdpi[$kx];
620                                        $not = $htmlsel[$kx];
621                                        $medias[$mw] = "@media $mw{{$not} span.$cid,{$not} span.$cid:after{background-image:url($file);}}";
622                                }
623                        }
624                        $prev_width = $w+1;
625                }
626
627                // One single CSS rule for old android browser (<3) which isn't able to manage override properly
628                // we chose JPG 320px width - 1.5x as a compromise
629                if ($wandroid){
630                        $file = $bkptImages[$wandroid]['15x'];
631                        $medias['android2'] = "html.android2 span.$cid,html.android2 span.$cid:after{background-image:url($file);}";
632                }
633
634                // Media-Queries
635                $style .= implode("",$medias);
636
637                $out = "<!--[if IE]>$img<![endif]-->\n";
638                $img = $this->setTagAttribute($img,"src",$fallback_file);
639                $img = $this->setTagAttribute($img,"class","adapt-img $class");
640                $img = $this->setTagAttribute($img,"onmousedown","adaptImgFix(this)");
641                // $img = setTagAttribute($img,"onkeydown","adaptImgFix(this)"); // usefull ?
642                $out .= "<!--[if !IE]><!--><span class=\"adapt-img-wrapper $cid $extension\">$img</span>\n<style>".$style."</style><!--<![endif]-->";
643
644                return $out;
645        }
646
647
648
649        /**
650         * Get height and width from an image file or <img> tag
651         * use width and height attributes of provided <img> tag if possible
652         * store getimagesize result in static to avoid multiple disk access if needed
653         *
654         * @param string $img
655         * @return array
656         *  (width,height)
657         */
658        protected function imgSize($img) {
659
660                static $largeur_img =array(), $hauteur_img= array();
661                $srcWidth = 0;
662                $srcHeight = 0;
663
664                $source = $this->tagAttribute($img,'src');
665
666                if (!$source) $source = $img;
667                else {
668                        $srcWidth = $this->tagAttribute($img,'width');
669                        $srcHeight = $this->tagAttribute($img,'height');
670                }
671
672                // never process on remote img
673                if (preg_match(';^(\w{3,7}://);', $source)){
674                        return array(0,0);
675                }
676                // remove timestamp on URL
677                if (($p=strpos($source,'?'))!==FALSE)
678                        $source=substr($source,0,$p);
679
680                if (isset($largeur_img[$source]))
681                        $srcWidth = $largeur_img[$source];
682                if (isset($hauteur_img[$source]))
683                        $srcHeight = $hauteur_img[$source];
684                if (!$srcWidth OR !$srcHeight){
685                        if (file_exists($source)
686                                AND $srcsize = @getimagesize($source)){
687                                if (!$srcWidth) $largeur_img[$source] = $srcWidth = $srcsize[0];
688                                if (!$srcHeight)        $hauteur_img[$source] = $srcHeight = $srcsize[1];
689                        }
690                }
691                return array($srcWidth,$srcHeight);
692        }
693
694
695        /**
696         * Find and get attribute value in an HTML tag
697         * Regexp from function extraire_attribut() in
698         * http://core.spip.org/projects/spip/repository/entry/spip/ecrire/inc/filtres.php#L2013
699         * @param $tag
700         *   html tag
701         * @param $attribute
702         *   attribute we look for
703         * @param $full
704         *   if true the function also returns the regexp match result
705         * @return array|string
706         */
707        protected function tagAttribute($tag, $attribute, $full = false) {
708                if (preg_match(
709                ',(^.*?<(?:(?>\s*)(?>[\w:.-]+)(?>(?:=(?:"[^"]*"|\'[^\']*\'|[^\'"]\S*))?))*?)(\s+'
710                .$attribute
711                .'(?:=\s*("[^"]*"|\'[^\']*\'|[^\'"]\S*))?)()([^>]*>.*),isS',
712
713                $tag, $r)) {
714                        if ($r[3][0] == '"' || $r[3][0] == "'") {
715                                $r[4] = substr($r[3], 1, -1);
716                                $r[3] = $r[3][0];
717                        } elseif ($r[3]!=='') {
718                                $r[4] = $r[3];
719                                $r[3] = '';
720                        } else {
721                                $r[4] = trim($r[2]);
722                        }
723                        $att = str_replace("&#39;", "'", $r[4]);
724                }
725                else
726                        $att = NULL;
727
728                if ($full)
729                        return array($att, $r);
730                else
731                        return $att;
732        }
733
734
735        /**
736         * change or insert an attribute of an html tag
737         *
738         * @param string $tag
739         *   html tag
740         * @param string $attribute
741         *   attribute name
742         * @param string $value
743         *   new value
744         * @param bool $protect
745         *   protect value if true (remove newlines and convert quotes)
746         * @param bool $removeEmpty
747         *   if true remove attribute from html tag if empty
748         * @return string
749         *   modified tag
750         */
751        protected function setTagAttribute($tag, $attribute, $value, $protect=true, $removeEmpty=false) {
752                // preparer l'attribut
753                // supprimer les &nbsp; etc mais pas les balises html
754                // qui ont un sens dans un attribut value d'un input
755                if ($protect) {
756                        $value = preg_replace(array(",\n,",",\s(?=\s),msS"),array(" ",""),strip_tags($value));
757                        $value = str_replace(array("'",'"',"<",">"),array('&#039;','&#034;','&lt;','&gt;'), $value);
758                }
759
760                // echapper les ' pour eviter tout bug
761                $value = str_replace("'", "&#039;", $value);
762                if ($removeEmpty AND strlen($value)==0)
763                        $insert = '';
764                else
765                        $insert = " $attribute='$value'";
766
767                list($old, $r) = $this->tagAttribute($tag, $attribute, true);
768
769                if ($old !== NULL) {
770                        // Remplacer l'ancien attribut du meme nom
771                        $tag = $r[1].$insert.$r[5];
772                }
773                else {
774                        // preferer une balise " />" (comme <img />)
775                        if (preg_match(',/>,', $tag))
776                                $tag = preg_replace(",\s?/>,S", $insert." />", $tag, 1);
777                        // sinon une balise <a ...> ... </a>
778                        else
779                                $tag = preg_replace(",\s?>,S", $insert.">", $tag, 1);
780                }
781
782                return $tag;
783        }
784
785        /**
786         * Provide Mime Type for Image file Extension
787         * @param $extension
788         * @return string
789         */
790        protected function extensionToMimeType($extension){
791                static $MimeTable = array(
792                        'jpg' => 'image/jpeg',
793                        'jpeg' => 'image/jpeg',
794                        'png' => 'image/png',
795                        'gif' => 'image/gif',
796                );
797
798                return (isset($MimeTable[$extension])?$MimeTable[$extension]:'image/jpeg');
799        }
800
801
802        /**
803         * Detect animated GIF : don't touch it
804         * http://it.php.net/manual/en/function.imagecreatefromgif.php#59787
805         *
806         * @param string $filename
807         * @return bool
808         */
809        protected function isAnimatedGif($filename){
810                $filecontents = file_get_contents($filename);
811
812                $str_loc = 0;
813                $count = 0;
814                while ($count<2) # There is no point in continuing after we find a 2nd frame
815                {
816
817                        $where1 = strpos($filecontents, "\x00\x21\xF9\x04", $str_loc);
818                        if ($where1===FALSE){
819                                break;
820                        } else {
821                                $str_loc = $where1+1;
822                                $where2 = strpos($filecontents, "\x00\x2C", $str_loc);
823                                if ($where2===FALSE){
824                                        break;
825                                } else {
826                                        if ($where1+8==$where2){
827                                                $count++;
828                                        }
829                                        $str_loc = $where2+1;
830                                }
831                        }
832                }
833
834                if ($count>1){
835                        return (true);
836
837                } else {
838                        return (false);
839                }
840        }
841
842        /**
843         * Embed image file in Base 64 URI
844         *
845         * @param string $filename
846         * @param int $maxsize
847         * @return string
848         *     URI Scheme of base64 if possible,
849         *     or URL from source file
850         */
851        function base64EmbedFile ($filename, $maxsize = 32768) {
852                $extension = substr(strrchr($filename,'.'),1);
853
854                if (!file_exists($filename)
855                        OR filesize($filename)>$maxsize
856                        OR !$content = file_get_contents($filename))
857                        return $filename;
858
859                $base64 = base64_encode($content);
860                $encoded = 'data:'.$this->extensionToMimeType($extension).';base64,'.$base64;
861
862                return $encoded;
863        }
864
865
866        /**
867         * Convert image to JPG and replace transparency with a background color
868         *
869         * @param string $source
870         *   source file name (or img tag)
871         * @param string $destDir
872         *   destination directory
873         * @param string $bgColor
874         *   hexa color
875         * @param int $quality
876         *   JPG quality
877         * @return resource
878         */
879        function img2JPG($source, $destDir, $bgColor='#000000', $quality=85) {
880                $infos = $this->readSourceImage($source, $destDir, 'jpg', true);
881
882                if (!$infos) return $source;
883
884                $couleurs = $this->colorHEX2RGB($bgColor);
885                $dr= $couleurs["red"];
886                $dv= $couleurs["green"];
887                $db= $couleurs["blue"];
888
889                $x_i = $infos["largeur"];
890                $y_i = $infos["hauteur"];
891
892                if ($infos["creer"]) {
893                        $im = @$infos["fonction_imagecreatefrom"]($infos["fichier"]);
894                        $this->imagepalettetotruecolor($im);
895                        $im_ = imagecreatetruecolor($x_i, $y_i);
896                        if ($infos["format_source"] == "gif" AND function_exists('ImageCopyResampled')) {
897                                // if was a transparent GIF
898                                // make a tansparent PNG
899                                @imagealphablending($im_, false);
900                                @imagesavealpha($im_,true);
901                                if (function_exists("imageAntiAlias")) imageAntiAlias($im_,true);
902                                @ImageCopyResampled($im_, $im, 0, 0, 0, 0, $x_i, $y_i, $x_i, $y_i);
903                                imagedestroy($im);
904                                $im = $im_;
905                        }
906
907                        // allocate background Color
908                        $color_t = ImageColorAllocate( $im_, $dr, $dv, $db);
909
910                        imagefill ($im_, 0, 0, $color_t);
911
912                        for ($x = 0; $x < $x_i; $x++) {
913                                for ($y=0; $y < $y_i; $y++) {
914
915                                        $rgb = ImageColorAt($im, $x, $y);
916                                        $a = ($rgb >> 24) & 0xFF;
917                                        $r = ($rgb >> 16) & 0xFF;
918                                        $g = ($rgb >> 8) & 0xFF;
919                                        $b = $rgb & 0xFF;
920
921                                        $a = (127-$a) / 127;
922
923                                        // faster if no transparency
924                                        if ($a == 1) {
925                                                $r = $r;
926                                                $g = $g;
927                                                $b = $b;
928                                        }
929                                        // faster if full transparency
930                                        else if ($a == 0) {
931                                                $r = $dr;
932                                                $g = $dv;
933                                                $b = $db;
934
935                                        }
936                                        else {
937                                                $r = round($a * $r + $dr * (1-$a));
938                                                $g = round($a * $g + $dv * (1-$a));
939                                                $b = round($a * $b + $db * (1-$a));
940                                        }
941                                        $a = (1-$a) *127;
942                                        $color = ImageColorAllocateAlpha( $im_, $r, $g, $b, $a);
943                                        imagesetpixel ($im_, $x, $y, $color);
944                                }
945                        }
946                        $this->saveGDImage($im_, $infos, $quality);
947                        imagedestroy($im_);
948                        imagedestroy($im);
949                }
950                return $infos["fichier_dest"];
951        }
952
953        /**
954         * Resize without bluring, and save image with needed quality if JPG image
955         * @author : Arno* from http://zone.spip.org/trac/spip-zone/browser/_plugins_/image_responsive/action/image_responsive.php
956         *
957         * @param string $source
958         * @param string $destDir
959         * @param int $maxWidth
960         * @param int $maxHeight
961         * @param int|null $quality
962         * @return string
963         */
964        function imgSharpResize($source, $destDir, $maxWidth = 0, $maxHeight = 0, $quality=null){
965                $infos = $this->readSourceImage($source, $destDir);
966                if (!$infos) return $source;
967
968                if ($maxWidth==0 AND $maxHeight==0)
969                        return $source;
970
971                if ($maxWidth==0) $maxWidth = 10000;
972                elseif ($maxHeight==0) $maxHeight = 10000;
973
974                $srcFile = $infos['fichier'];
975                $srcExt = $infos['format_source'];
976
977                $destination = dirname($infos['fichier_dest']) . "/" . basename($infos['fichier_dest'], ".".$infos["format_dest"]);
978
979                // compute width & height
980                $srcWidth = $infos['largeur'];
981                $srcHeight = $infos['hauteur'];
982                list($destWidth,$destHeight) = $this->computeImageSize($srcWidth, $srcHeight, $maxWidth, $maxHeight);
983
984                if ($infos['creer']==false)
985                        return $infos['fichier_dest'];
986
987                // If source image is smaller than desired size, keep source
988                if ($srcWidth
989                  AND $srcWidth<=$destWidth
990                  AND $srcHeight<=$destHeight){
991
992                        $infos['format_dest'] = $srcExt;
993                        $infos['fichier_dest'] = $destination.".".$srcExt;
994                        @copy($srcFile, $infos['fichier_dest']);
995
996                }
997                else {
998                        if (defined('_IMG_GD_MAX_PIXELS') AND _IMG_GD_MAX_PIXELS AND $srcWidth*$srcHeight>_IMG_GD_MAX_PIXELS){
999                                spip_log("vignette gd1/gd2 impossible : " . $srcWidth*$srcHeight . "pixels");
1000                                return $srcFile;
1001                        }
1002                        $destExt = $infos['format_dest'];
1003                        if (!$destExt){
1004                                spip_log("pas de format pour $srcFile");
1005                                return $srcFile;
1006                        }
1007
1008                        $fonction_imagecreatefrom = $infos['fonction_imagecreatefrom'];
1009
1010                        if (!function_exists($fonction_imagecreatefrom))
1011                                return $srcFile;
1012                        $srcImage = @$fonction_imagecreatefrom($srcFile);
1013                        if (!$srcImage){
1014                                spip_log("echec gd1/gd2");
1015                                return $srcFile;
1016                        }
1017
1018                        // Initialization of dest image
1019                        $destImage = ImageCreateTrueColor($destWidth, $destHeight);
1020
1021                        // Copy and resize source image
1022                        $ok = false;
1023                        if (function_exists('ImageCopyResampled')){
1024                                // if transparent GIF, keep the transparency
1025                                if ($srcExt=="gif"){
1026                                        $transparent_index = ImageColorTransparent($srcImage);
1027                                        if($transparent_index!=(-1)){
1028                                                $transparent_color = ImageColorsForIndex($srcImage,$transparent_index);
1029                                                if(!empty($transparent_color)) {
1030                                                        $transparent_new = ImageColorAllocate($destImage,$transparent_color['red'],$transparent_color['green'],$transparent_color['blue']);
1031                                                        $transparent_new_index = ImageColorTransparent($destImage,$transparent_new);
1032                                                        ImageFill($destImage, 0,0, $transparent_new_index);
1033                                                }
1034                                        }
1035                                }
1036                                if ($destExt=="png"){
1037                                        // keep transparency
1038                                        if (function_exists("imageAntiAlias")) imageAntiAlias($destImage, true);
1039                                        @imagealphablending($destImage, false);
1040                                        @imagesavealpha($destImage, true);
1041                                }
1042                                $ok = @ImageCopyResampled($destImage, $srcImage, 0, 0, 0, 0, $destWidth, $destHeight, $srcWidth, $srcHeight);
1043                        }
1044                        if (!$ok)
1045                                $ok = ImageCopyResized($destImage, $srcImage, 0, 0, 0, 0, $destWidth, $destHeight, $srcWidth, $srcHeight);
1046
1047                        if ($destExt=="jpg" && function_exists('imageconvolution')){
1048                                $intSharpness = $this->computeSharpCoeff($srcWidth, $destWidth);
1049                                $arrMatrix = array(
1050                                        array(-1, -2, -1),
1051                                        array(-2, $intSharpness+12, -2),
1052                                        array(-1, -2, -1)
1053                                );
1054                                imageconvolution($destImage, $arrMatrix, $intSharpness, 0);
1055                        }
1056                        // save destination image
1057                        $this->saveGDImage($destImage, $infos, $quality);
1058
1059                        if ($srcImage)
1060                                ImageDestroy($srcImage);
1061                        ImageDestroy($destImage);
1062                }
1063
1064                return $infos['fichier_dest'];
1065
1066        }
1067
1068        /**
1069         * @author : Arno* from http://zone.spip.org/trac/spip-zone/browser/_plugins_/image_responsive/action/image_responsive.php
1070         *
1071         * @param int $intOrig
1072         * @param int $intFinal
1073         * @return mixed
1074         */
1075        function computeSharpCoeff($intOrig, $intFinal) {
1076          $intFinal = $intFinal * (750.0 / $intOrig);
1077          $intA     = 52;
1078          $intB     = -0.27810650887573124;
1079          $intC     = .00047337278106508946;
1080          $intRes   = $intA + $intB * $intFinal + $intC * $intFinal * $intFinal;
1081          return max(round($intRes), 0);
1082        }
1083
1084        /**
1085         * Read and preprocess informations about source image
1086         *
1087         * @param string $img
1088         *              HTML img tag <img src=... /> OR source filename
1089         * @param string $destDir
1090         *              Destination dir of new image
1091         * @param null|string $outputFormat
1092         *              forced extension of output image file : jpg, png, gif
1093         * @param bool $hashfilename
1094         *    if true the final file name is a md5 of source file name
1095         *    otherwise final file name keeps the source path and file starting from $destDir
1096         * @return bool|array
1097         *              false in case of error
1098         *    array of image information otherwise
1099         * @throws Exception
1100         */
1101        protected function readSourceImage($img, $destDir, $outputFormat = null, $hashfilename = false) {
1102                if (strlen($img)==0) return false;
1103                $ret = array();
1104
1105                $source = trim($this->tagAttribute($img, 'src'));
1106                if (strlen($source) < 1){
1107                        $source = $img;
1108                        $img = "<img src='$source' />";
1109                }
1110                # gerer img src="data:....base64"
1111                # don't process base64
1112                else if (preg_match('@^data:image/(jpe?g|png|gif);base64,(.*)$@isS', $source)) {
1113                        return false;
1114                }
1115
1116                // don't process distant images
1117                if (preg_match(';^(\w{3,7}://);', $source)){
1118                        return false;
1119                }
1120
1121                // remove timestamp as query string
1122                $source=preg_replace(',[?][0-9]+$,','',$source);
1123
1124                $extension_dest = "";
1125                if (preg_match(",\.(gif|jpe?g|png)($|[?]),i", $source, $regs)) {
1126                        $extension = strtolower($regs[1]);
1127                        $extension_dest = $extension;
1128                }
1129                if (!is_null($outputFormat)) $extension_dest = $outputFormat;
1130
1131                if (!$extension_dest) return false;
1132
1133                if (@file_exists($source)){
1134                        list ($ret["largeur"],$ret["hauteur"]) = $this->imgSize($img);
1135                        $date_src = @filemtime($source);
1136                }
1137                else
1138                        return false;
1139
1140                // error if no known size
1141                if (!($ret["hauteur"] OR $ret["largeur"]))
1142                        return false;
1143
1144
1145                // dest filename : md5(source) or full source path name
1146                $nom_fichier = (!$hashfilename?(substr($source, 0, strlen($source) - (strlen($extension) + 1))):md5($source));
1147                $fichier_dest = rtrim($destDir,"/") . "/" . $nom_fichier . "." . $extension_dest;
1148
1149                $creer = true;
1150                if (@file_exists($f = $fichier_dest)){
1151                        if (filemtime($f)>=$date_src)
1152                                $creer = false;
1153                }
1154                // mkdir complete path if needed
1155                if ($creer
1156                  AND !is_dir($d=dirname($fichier_dest))){
1157                        mkdir($d,0777,true);
1158                        if (!is_dir($d)){
1159                                throw new Exception("Unable to mkdir {$d}");
1160                        }
1161                }
1162
1163                $ret["fonction_imagecreatefrom"] = "imagecreatefrom".($extension != 'jpg' ? $extension : 'jpeg');
1164                $ret["fichier"] = $source;
1165                $ret["fichier_dest"] = $fichier_dest;
1166                $ret["format_source"] = ($extension != 'jpeg' ? $extension : 'jpg');
1167                $ret["format_dest"] = $extension_dest;
1168                $ret["date_src"] = $date_src;
1169                $ret["creer"] = $creer;
1170                $ret["tag"] = $img;
1171
1172                if (!function_exists($ret["fonction_imagecreatefrom"])) return false;
1173                return $ret;
1174        }
1175
1176        /**
1177         * Compute new image size according to max Width and max Height and initial width/height ratio
1178         * @param int $srcWidth
1179         * @param int $srcHeight
1180         * @param int $maxWidth
1181         * @param int $maxHeight
1182         * @return array
1183         */
1184        function computeImageSize($srcWidth, $srcHeight, $maxWidth, $maxHeight) {
1185                $ratioWidth = $srcWidth/$maxWidth;
1186                $ratioHeight = $srcHeight/$maxHeight;
1187
1188                if ($ratioWidth <=1 AND $ratioHeight <=1) {
1189                        return array($srcWidth,$srcHeight);
1190                }
1191                else if ($ratioWidth < $ratioHeight) {
1192                        $destWidth = intval(ceil($srcWidth/$ratioHeight));
1193                        $destHeight = $maxHeight;
1194                }
1195                else {
1196                        $destWidth = $maxWidth;
1197                        $destHeight = intval(ceil($srcHeight/$ratioWidth));
1198                }
1199                return array ($destWidth, $destHeight);
1200        }
1201
1202        /**
1203         * SaveAffiche ou sauvegarde une image au format PNG
1204         * Utilise les fonctions spécifiques GD.
1205         *
1206         * @param resource $img
1207         *   GD image resource
1208         * @param array $infos
1209         *   image description
1210         * @param int|null $quality
1211         *   compression quality for JPG images
1212         * @return bool
1213         */
1214        protected function saveGDImage($img, $infos, $quality=null) {
1215                $fichier = $infos['fichier_dest'];
1216                $tmp = $fichier.".tmp";
1217                switch($infos['format_dest']){
1218                        case "gif":
1219                                $ret = imagegif($img,$tmp);
1220                                break;
1221                        case "png":
1222                                $ret = imagepng($img,$tmp);
1223                                break;
1224                        case "jpg":
1225                        case "jpeg":
1226                                $ret = imagejpeg($img,$tmp,$quality);
1227                                break;
1228                }
1229                if(file_exists($tmp)){
1230                        $taille_test = getimagesize($tmp);
1231                        if ($taille_test[0] < 1) return false;
1232
1233                        @unlink($fichier); // le fichier peut deja exister
1234                        @rename($tmp, $fichier);
1235                        return $ret;
1236                }
1237                return false;
1238        }
1239
1240
1241        /**
1242         * Convert indexed colors image to true color image
1243         * available in PHP 5.5+ http://www.php.net/manual/fr/function.imagepalettetotruecolor.php
1244         * @param resource $img
1245         * @return bool
1246         */
1247        protected function imagepalettetotruecolor(&$img) {
1248                if (function_exists("imagepalettetotruecolor"))
1249                        return imagepalettetotruecolor($img);
1250
1251                if ($img AND !imageistruecolor($img) AND function_exists('imagecreatetruecolor')) {
1252                        $w = imagesx($img);
1253                        $h = imagesy($img);
1254                        $img1 = imagecreatetruecolor($w,$h);
1255                        // keep alpha layer if possible
1256                        if(function_exists('ImageCopyResampled')) {
1257                                if (function_exists("imageAntiAlias")) imageAntiAlias($img1,true);
1258                                @imagealphablending($img1, false);
1259                                @imagesavealpha($img1,true);
1260                                @ImageCopyResampled($img1, $img, 0, 0, 0, 0, $w, $h, $w, $h);
1261                        } else {
1262                                imagecopy($img1,$img,0,0,0,0,$w,$h);
1263                        }
1264
1265                        $img = $img1;
1266                        return true;
1267                }
1268                return false;
1269        }
1270
1271
1272        /**
1273         * Translate HTML color to hexa color
1274         * @param string $color
1275         * @return string
1276         */
1277        protected function colorHTML2Hex($color){
1278                static $html_colors=array(
1279                        'aqua'=>'00FFFF','black'=>'000000','blue'=>'0000FF','fuchsia'=>'FF00FF','gray'=>'808080','green'=>'008000','lime'=>'00FF00','maroon'=>'800000',
1280                        'navy'=>'000080','olive'=>'808000','purple'=>'800080','red'=>'FF0000','silver'=>'C0C0C0','teal'=>'008080','white'=>'FFFFFF','yellow'=>'FFFF00');
1281                if (isset($html_colors[$lc=strtolower($color)]))
1282                        return $html_colors[$lc];
1283                return $color;
1284        }
1285
1286        /**
1287         * Translate hexa color to RGB
1288         * @param string $color
1289         *   hexa color (#000000 à #FFFFFF).
1290         * @return array
1291         */
1292        protected function colorHEX2RGB($color) {
1293                $color = $this->colorHTML2Hex($color);
1294                $color = ltrim($color,"#");
1295                $retour["red"] = hexdec(substr($color, 0, 2));
1296                $retour["green"] = hexdec(substr($color, 2, 2));
1297                $retour["blue"] = hexdec(substr($color, 4, 2));
1298
1299                return $retour;
1300        }
1301
1302}
Note: See TracBrowser for help on using the repository browser.