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

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

Up lib, renommage variable, et mise a jour de la config pour permettre de configurer la largeur des miniatures

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