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

Last change on this file since 115788 was 115788, checked in by cedric@…, 8 months ago

Mise a jour de la lib AdaptiveImage? : meilleure optimisation de la vignette (taille plus petite+filtre blur) et markup <picture> au lieu de <span> (sans modif de la technique)

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