1 | <?php |
---|
2 | /** |
---|
3 | * AdaptiveImages |
---|
4 | * |
---|
5 | * @version 1.7.2 |
---|
6 | * @copyright 2013 |
---|
7 | * @author Nursit |
---|
8 | * @licence GNU/GPL3 |
---|
9 | * @source https://github.com/nursit/AdaptiveImages |
---|
10 | */ |
---|
11 | |
---|
12 | |
---|
13 | class 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 = 10; |
---|
38 | |
---|
39 | /** |
---|
40 | * JPG compression quality for 1x JPG images |
---|
41 | * @var int |
---|
42 | */ |
---|
43 | protected $x10JpgQuality = 75; |
---|
44 | |
---|
45 | /** |
---|
46 | * JPG compression quality for 1.5x JPG images |
---|
47 | * @var int |
---|
48 | */ |
---|
49 | protected $x15JpgQuality = 65; |
---|
50 | |
---|
51 | /** |
---|
52 | * JPG compression quality for 2x JPG images |
---|
53 | * @var int |
---|
54 | */ |
---|
55 | protected $x20JpgQuality = 45; |
---|
56 | |
---|
57 | /** |
---|
58 | * Breakpoints width for image generation |
---|
59 | * @var array |
---|
60 | */ |
---|
61 | protected $defaultBkpts = array(160,320,480,640,960,1440); |
---|
62 | |
---|
63 | /** |
---|
64 | * Maximum display width for images |
---|
65 | * @var int |
---|
66 | */ |
---|
67 | protected $maxWidth1x = 640; |
---|
68 | |
---|
69 | /** |
---|
70 | * Minimum display width for adaptive images (smaller will be unchanged) |
---|
71 | * @var int |
---|
72 | */ |
---|
73 | protected $minWidth1x = 320; |
---|
74 | |
---|
75 | /** |
---|
76 | * 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 = 640; |
---|
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;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;background-size:100% auto;background-repeat:no-repeat;line-height:1px;}" |
---|
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 |
---|
330 | function 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()}}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)})(); |
---|
331 | JS; |
---|
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 | * @return string |
---|
452 | * name of image file |
---|
453 | * @throws Exception |
---|
454 | */ |
---|
455 | protected function processBkptImage($src, $wkpt, $wx, $x, $extension, $force=false){ |
---|
456 | $dir_dest = $this->destDirectory."$wkpt/$x/"; |
---|
457 | $dest = $dir_dest . $this->adaptedSrcToURL($src); |
---|
458 | |
---|
459 | if (($exist=file_exists($dest)) AND filemtime($dest)>=filemtime($src)) |
---|
460 | return $dest; |
---|
461 | |
---|
462 | $force = ($force?true:!$this->onDemandImages); |
---|
463 | |
---|
464 | // if file already exists but too old, delete it if we don't want to generate it now |
---|
465 | // it will be generated on first request |
---|
466 | if ($exist AND !$force) |
---|
467 | @unlink($dest); |
---|
468 | |
---|
469 | if (!$force) |
---|
470 | return $dest; |
---|
471 | |
---|
472 | switch($x){ |
---|
473 | case '10x': |
---|
474 | $quality = $this->x10JpgQuality; |
---|
475 | break; |
---|
476 | case '15x': |
---|
477 | $quality = $this->x15JpgQuality; |
---|
478 | break; |
---|
479 | case '20x': |
---|
480 | $quality = $this->x20JpgQuality; |
---|
481 | break; |
---|
482 | } |
---|
483 | |
---|
484 | $i = $this->imgSharpResize($src,$dest,$wx,10000,$quality); |
---|
485 | if ($i AND $i!==$dest AND $i!==$src){ |
---|
486 | throw new Exception("Error in imgSharpResize: return \"$i\" whereas \"$dest\" expected"); |
---|
487 | } |
---|
488 | if (!file_exists($i)){ |
---|
489 | throw new Exception("Error file \"$i\" not found: check the right to write in ".$this->destDirectory); |
---|
490 | } |
---|
491 | return $i; |
---|
492 | } |
---|
493 | |
---|
494 | |
---|
495 | /** |
---|
496 | * Build an image variant from it's URL |
---|
497 | * this function is used when $this->onDemandImages==true |
---|
498 | * needs a RewriteRule such as following and a router to call this function on first request |
---|
499 | * |
---|
500 | * RewriteRule \badapt-img/(\d+/\d\dx/.*)$ spip.php?action=adapt_img&arg=$1 [QSA,L] |
---|
501 | * |
---|
502 | * @param string $URLPath |
---|
503 | * @param string $mime |
---|
504 | * @return string |
---|
505 | * @throws Exception |
---|
506 | */ |
---|
507 | protected function processBkptImageFromPath($URLPath,&$mime){ |
---|
508 | $base = $this->destDirectory; |
---|
509 | $path = $URLPath; |
---|
510 | // if base path is provided, remove it |
---|
511 | if (strncmp($path,$base,strlen($base))==0) |
---|
512 | $path = substr($path,strlen($base)); |
---|
513 | |
---|
514 | $path = explode("/",$path); |
---|
515 | $wkpt = intval(array_shift($path)); |
---|
516 | $x = array_shift($path); |
---|
517 | $url = implode("/",$path); |
---|
518 | |
---|
519 | // translate URL part to file path |
---|
520 | $src = $this->adaptedURLToSrc($url); |
---|
521 | |
---|
522 | $parts = pathinfo($src); |
---|
523 | $extension = strtolower($parts['extension']); |
---|
524 | $mime = $this->extensionToMimeType($extension); |
---|
525 | $dpi = array('10x'=>1,'15x'=>1.5,'20x'=>2); |
---|
526 | |
---|
527 | // check that path is well formed |
---|
528 | if (!$wkpt |
---|
529 | OR !isset($dpi[$x]) |
---|
530 | OR !file_exists($src) |
---|
531 | OR !$mime){ |
---|
532 | throw new Exception("Unable to build adapted image $URLPath"); |
---|
533 | } |
---|
534 | $wx = intval(round($wkpt * $dpi[$x])); |
---|
535 | |
---|
536 | $file = $this->processBkptImage($src, $wkpt, $wx, $x, $extension, true); |
---|
537 | return $file; |
---|
538 | } |
---|
539 | |
---|
540 | |
---|
541 | /** |
---|
542 | * Process one single <img> tag : |
---|
543 | * extract informations of src attribute |
---|
544 | * and data-src-mobile attribute if provided |
---|
545 | * compute images versions for provided breakpoints |
---|
546 | * |
---|
547 | * Don't do anything if img width is lower than $this->minWidth1x |
---|
548 | * or img filesize smaller than $this->minFileSize |
---|
549 | * |
---|
550 | * @param string $img |
---|
551 | * html img tag |
---|
552 | * @param array $bkpt |
---|
553 | * breakpoints |
---|
554 | * @param int $maxWidth1x |
---|
555 | * max display with of image (in 1x) |
---|
556 | * @return string |
---|
557 | * html markup : original markup or adapted markup |
---|
558 | */ |
---|
559 | protected function processImgTag($img, $bkpt, $maxWidth1x){ |
---|
560 | if (!$img) return $img; |
---|
561 | |
---|
562 | // don't do anyting if has adapt-img (already adaptive) or no-adapt-img class (no adaptative needed) |
---|
563 | if (strpos($img, "adapt-img")!==false) |
---|
564 | return $img; |
---|
565 | if (is_null($bkpt) OR !is_array($bkpt)) |
---|
566 | $bkpt = $this->defaultBkpts; |
---|
567 | |
---|
568 | list($w,$h) = $this->imgSize($img); |
---|
569 | // Don't do anything if img is to small or unknown width |
---|
570 | if (!$w OR $w<=$this->minWidth1x) return $img; |
---|
571 | |
---|
572 | $src = trim($this->tagAttribute($img, 'src')); |
---|
573 | if (strlen($src)<1){ |
---|
574 | $src = $img; |
---|
575 | $img = "<img src='".$src."' />"; |
---|
576 | } |
---|
577 | $srcMobile = $this->tagAttribute($img, 'data-src-mobile'); |
---|
578 | |
---|
579 | // don't do anything with data-URI images |
---|
580 | if (strncmp($src, "data:", 5)==0) |
---|
581 | return $img; |
---|
582 | |
---|
583 | $src = $this->URL2filepath($src); |
---|
584 | if (!$src) return $img; |
---|
585 | |
---|
586 | // Don't do anything if img filesize is to small |
---|
587 | $filesize=@filesize($src); |
---|
588 | if ($filesize AND $filesize<$this->minFileSize) return $img; |
---|
589 | |
---|
590 | if ($srcMobile) |
---|
591 | $srcMobile = $this->URL2filepath($srcMobile); |
---|
592 | |
---|
593 | $images = array(); |
---|
594 | if ($w<end($bkpt)) |
---|
595 | $images[$w] = array( |
---|
596 | '10x' => $src, |
---|
597 | '15x' => $src, |
---|
598 | '20x' => $src, |
---|
599 | ); |
---|
600 | |
---|
601 | // don't do anyting if we can't find file |
---|
602 | if (!file_exists($src)) |
---|
603 | return $img; |
---|
604 | |
---|
605 | $parts = pathinfo($src); |
---|
606 | $extension = $parts['extension']; |
---|
607 | |
---|
608 | // don't do anyting if it's an animated GIF |
---|
609 | if ($extension=="gif" AND $this->isAnimatedGif($src)) |
---|
610 | return $img; |
---|
611 | |
---|
612 | // build images (or at least URLs of images) on breakpoints |
---|
613 | $fallback = $src; |
---|
614 | $wfallback = $w; |
---|
615 | $dpi = array('10x' => 1, '15x' => 1.5, '20x' => 2); |
---|
616 | $wk = 0; |
---|
617 | foreach ($bkpt as $wk){ |
---|
618 | if ($wk>$w) break; |
---|
619 | $is_mobile = (($srcMobile AND $wk<=$this->maxWidthMobileVersion) ? true : false); |
---|
620 | foreach ($dpi as $k => $x){ |
---|
621 | $wkx = intval(round($wk*$x)); |
---|
622 | if ($wkx>$w) |
---|
623 | $images[$wk][$k] = $src; |
---|
624 | else { |
---|
625 | $images[$wk][$k] = $this->processBkptImage($is_mobile ? $srcMobile : $src, $wk, $wkx, $k, $extension); |
---|
626 | } |
---|
627 | } |
---|
628 | if ($wk<=$maxWidth1x |
---|
629 | AND ($wk<=$this->maxWidthFallbackVersion) |
---|
630 | AND ($is_mobile OR !$srcMobile)){ |
---|
631 | $fallback = $images[$wk]['10x']; |
---|
632 | $wfallback = $wk; |
---|
633 | } |
---|
634 | } |
---|
635 | |
---|
636 | // Build the fallback img : High-compressed JPG |
---|
637 | // Start from the mobile version if available or from the larger version otherwise |
---|
638 | if ($wk>$w |
---|
639 | AND $w<$maxWidth1x |
---|
640 | AND $w<$this->maxWidthFallbackVersion){ |
---|
641 | $fallback = $images[$w]['10x']; |
---|
642 | $wfallback = $w; |
---|
643 | } |
---|
644 | |
---|
645 | |
---|
646 | // if $this->onDemandImages == true image has not been built yet |
---|
647 | // in this case ask for immediate generation |
---|
648 | if (!file_exists($fallback)){ |
---|
649 | $mime = ""; // not used here |
---|
650 | $this->processBkptImageFromPath($fallback, $mime); |
---|
651 | } |
---|
652 | |
---|
653 | // $this->lowsrcJpgQuality give a base quality for a 450kpx image size |
---|
654 | // quality is varying around this value (+/- 50%) depending of image pixel size |
---|
655 | // in order to limit the weight of fallback (empirical rule) |
---|
656 | $q = round($this->lowsrcJpgQuality-((min($maxWidth1x, $wfallback)*$h/$w*min($maxWidth1x, $wfallback))/75000-6)); |
---|
657 | $q = min($q, round($this->lowsrcJpgQuality)*1.5); |
---|
658 | $q = max($q, round($this->lowsrcJpgQuality)*0.5); |
---|
659 | $images["fallback"] = $this->img2JPG($fallback, $this->destDirectory."fallback/", $this->lowsrcJpgBgColor, $q); |
---|
660 | |
---|
661 | // limit $src image width to $maxWidth1x for old IE |
---|
662 | $src = $this->processBkptImage($src,$maxWidth1x,$maxWidth1x,'10x',$extension,true); |
---|
663 | list($w,$h) = $this->imgSize($src); |
---|
664 | $img = $this->setTagAttribute($img,"src",$this->filepath2URL($src)); |
---|
665 | $img = $this->setTagAttribute($img,"width",$w); |
---|
666 | $img = $this->setTagAttribute($img,"height",$h); |
---|
667 | |
---|
668 | // ok, now build the markup |
---|
669 | return $this->imgAdaptiveMarkup($img, $images, $w, $h, $extension, $maxWidth1x); |
---|
670 | } |
---|
671 | |
---|
672 | |
---|
673 | /** |
---|
674 | * Build html markup with CSS rules in <style> tag |
---|
675 | * from provided img tag an array of bkpt images |
---|
676 | * |
---|
677 | * @param string $img |
---|
678 | * source img tag |
---|
679 | * @param array $bkptImages |
---|
680 | * falbback => file |
---|
681 | * width => |
---|
682 | * 10x => file |
---|
683 | * 15x => file |
---|
684 | * 20x => file |
---|
685 | * @param int $width |
---|
686 | * @param int $height |
---|
687 | * @param string $extension |
---|
688 | * @param int $maxWidth1x |
---|
689 | * @return string |
---|
690 | */ |
---|
691 | function imgAdaptiveMarkup($img, $bkptImages, $width, $height, $extension, $maxWidth1x){ |
---|
692 | $originalClass = $class = $this->tagAttribute($img,"class"); |
---|
693 | if (strpos($class,"adapt-img")!==false) return $img; |
---|
694 | ksort($bkptImages); |
---|
695 | $cid = "c".crc32(serialize($bkptImages)); |
---|
696 | $style = ""; |
---|
697 | $img = $this->setTagAttribute($img,"class","adapt-img-ie $class"); |
---|
698 | |
---|
699 | // provided fallback image? |
---|
700 | $fallback_file = ""; |
---|
701 | if (isset($bkptImages['fallback'])){ |
---|
702 | $fallback_file = $bkptImages['fallback']; |
---|
703 | unset($bkptImages['fallback']); |
---|
704 | } |
---|
705 | // else we use the smallest one |
---|
706 | if (!$fallback_file){ |
---|
707 | $fallback_file = reset($bkptImages); |
---|
708 | $fallback_file = $fallback_file['10x']; |
---|
709 | } |
---|
710 | // embed fallback as a DATA URI if not more than 32ko |
---|
711 | $fallback_file = $this->base64EmbedFile($fallback_file); |
---|
712 | |
---|
713 | $prev_width = 0; |
---|
714 | $medias = array(); |
---|
715 | $lastw = array_keys($bkptImages); |
---|
716 | $lastw = end($lastw); |
---|
717 | $wandroid = 0; |
---|
718 | $islast = false; |
---|
719 | foreach ($bkptImages as $w=>$files){ |
---|
720 | if ($w==$lastw) {$islast = true;} |
---|
721 | if ($w<=$this->maxWidthMobileVersion) $wandroid = $w; |
---|
722 | // use min-width and max-width in order to avoid override |
---|
723 | if ($prev_width<$maxWidth1x){ |
---|
724 | $hasmax = (($islast OR $w>=$maxWidth1x)?false:true); |
---|
725 | $mw = ($prev_width?"and (min-width:{$prev_width}px)":"").($hasmax?" and (max-width:{$w}px)":""); |
---|
726 | $htmlsel = "html:not(.android2)"; |
---|
727 | $htmlsel = array( |
---|
728 | '10x' => "$htmlsel", |
---|
729 | '15x' => "$htmlsel:not(.aislow)", |
---|
730 | '20x' => "$htmlsel:not(.aislow)", |
---|
731 | ); |
---|
732 | } |
---|
733 | $mwdpi = array( |
---|
734 | '10x' => "screen $mw", |
---|
735 | '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", |
---|
736 | '20x' => "screen and (-webkit-min-device-pixel-ratio: 2) $mw,screen and (min--moz-device-pixel-ratio: 2) $mw", |
---|
737 | ); |
---|
738 | foreach($files as $kx=>$file){ |
---|
739 | if (isset($mwdpi[$kx])){ |
---|
740 | $mw = $mwdpi[$kx]; |
---|
741 | $not = $htmlsel[$kx]; |
---|
742 | $url = $this->filepath2URL($file); |
---|
743 | $medias[$mw] = "@media $mw{{$not} .$cid,{$not} .$cid:after{background-image:url($url);}}"; |
---|
744 | } |
---|
745 | } |
---|
746 | $prev_width = $w+1; |
---|
747 | } |
---|
748 | |
---|
749 | // One single CSS rule for old android browser (<3) which isn't able to manage override properly |
---|
750 | // we chose JPG 320px width - 1.5x as a compromise |
---|
751 | if ($wandroid){ |
---|
752 | $file = $bkptImages[$wandroid]['15x']; |
---|
753 | $url = $this->filepath2URL($file); |
---|
754 | $medias['android2'] = "html.android2 .$cid,html.android2 .$cid:after{background-image:url($url);}"; |
---|
755 | } |
---|
756 | |
---|
757 | // Media-Queries |
---|
758 | $style .= implode("",$medias); |
---|
759 | |
---|
760 | |
---|
761 | $originalStyle = $this->tagAttribute($img,"style"); |
---|
762 | $out = "<!--[if IE]>$img<![endif]-->\n"; |
---|
763 | |
---|
764 | $img = $this->setTagAttribute($img,"src",$fallback_file); |
---|
765 | $img = $this->setTagAttribute($img,"class","adapt-img $class"); |
---|
766 | $img = $this->setTagAttribute($img,"onmousedown","adaptImgFix(this)"); |
---|
767 | // $img = setTagAttribute($img,"onkeydown","adaptImgFix(this)"); // useful ? |
---|
768 | |
---|
769 | // markup can be adjusted in hook, depending on style and class |
---|
770 | $markup = "<span class=\"adapt-img-wrapper $cid $extension\">$img</span>"; |
---|
771 | $markup = $this->imgMarkupHook($markup,$originalClass,$originalStyle); |
---|
772 | |
---|
773 | $out .= "<!--[if !IE]><!-->$markup\n<style>".$style."</style><!--<![endif]-->"; |
---|
774 | |
---|
775 | return $out; |
---|
776 | } |
---|
777 | |
---|
778 | |
---|
779 | |
---|
780 | /** |
---|
781 | * Get height and width from an image file or <img> tag |
---|
782 | * use width and height attributes of provided <img> tag if possible |
---|
783 | * store getimagesize result in static to avoid multiple disk access if needed |
---|
784 | * |
---|
785 | * @param string $img |
---|
786 | * @return array |
---|
787 | * (width,height) |
---|
788 | */ |
---|
789 | protected function imgSize($img) { |
---|
790 | |
---|
791 | static $largeur_img =array(), $hauteur_img= array(); |
---|
792 | $srcWidth = 0; |
---|
793 | $srcHeight = 0; |
---|
794 | |
---|
795 | $source = $this->tagAttribute($img,'src'); |
---|
796 | |
---|
797 | if (!$source) $source = $img; |
---|
798 | else { |
---|
799 | $srcWidth = $this->tagAttribute($img,'width'); |
---|
800 | $srcHeight = $this->tagAttribute($img,'height'); |
---|
801 | if ($srcWidth AND $srcHeight) |
---|
802 | return array($srcWidth,$srcHeight); |
---|
803 | $source = $this->URL2filepath($source); |
---|
804 | } |
---|
805 | |
---|
806 | // never process on remote img |
---|
807 | if (!$source OR preg_match(';^(\w{3,7}://);', $source)){ |
---|
808 | return array(0,0); |
---|
809 | } |
---|
810 | |
---|
811 | if (isset($largeur_img[$source])) |
---|
812 | $srcWidth = $largeur_img[$source]; |
---|
813 | if (isset($hauteur_img[$source])) |
---|
814 | $srcHeight = $hauteur_img[$source]; |
---|
815 | if (!$srcWidth OR !$srcHeight){ |
---|
816 | if (file_exists($source) |
---|
817 | AND $srcsize = @getimagesize($source)){ |
---|
818 | if (!$srcWidth) $largeur_img[$source] = $srcWidth = $srcsize[0]; |
---|
819 | if (!$srcHeight) $hauteur_img[$source] = $srcHeight = $srcsize[1]; |
---|
820 | } |
---|
821 | } |
---|
822 | return array($srcWidth,$srcHeight); |
---|
823 | } |
---|
824 | |
---|
825 | |
---|
826 | /** |
---|
827 | * Find and get attribute value in an HTML tag |
---|
828 | * Regexp from function extraire_attribut() in |
---|
829 | * https://core.spip.net/projects/spip/repository/entry/spip/ecrire/inc/filtres.php#L2013 |
---|
830 | * @param $tag |
---|
831 | * html tag |
---|
832 | * @param $attribute |
---|
833 | * attribute we look for |
---|
834 | * @param $full |
---|
835 | * if true the function also returns the regexp match result |
---|
836 | * @return array|string |
---|
837 | */ |
---|
838 | protected function tagAttribute($tag, $attribute, $full = false) { |
---|
839 | if (preg_match( |
---|
840 | ',(^.*?<(?:(?>\s*)(?>[\w:.-]+)(?>(?:=(?:"[^"]*"|\'[^\']*\'|[^\'"]\S*))?))*?)(\s+' |
---|
841 | .$attribute |
---|
842 | .'(?:=\s*("[^"]*"|\'[^\']*\'|[^\'"]\S*))?)()([^>]*>.*),isS', |
---|
843 | |
---|
844 | $tag, $r)) { |
---|
845 | if ($r[3][0] == '"' || $r[3][0] == "'") { |
---|
846 | $r[4] = substr($r[3], 1, -1); |
---|
847 | $r[3] = $r[3][0]; |
---|
848 | } elseif ($r[3]!=='') { |
---|
849 | $r[4] = $r[3]; |
---|
850 | $r[3] = ''; |
---|
851 | } else { |
---|
852 | $r[4] = trim($r[2]); |
---|
853 | } |
---|
854 | $att = str_replace("'", "'", $r[4]); |
---|
855 | } |
---|
856 | else |
---|
857 | $att = NULL; |
---|
858 | |
---|
859 | if ($full) |
---|
860 | return array($att, $r); |
---|
861 | else |
---|
862 | return $att; |
---|
863 | } |
---|
864 | |
---|
865 | |
---|
866 | /** |
---|
867 | * change or insert an attribute of an html tag |
---|
868 | * |
---|
869 | * @param string $tag |
---|
870 | * html tag |
---|
871 | * @param string $attribute |
---|
872 | * attribute name |
---|
873 | * @param string $value |
---|
874 | * new value |
---|
875 | * @param bool $protect |
---|
876 | * protect value if true (remove newlines and convert quotes) |
---|
877 | * @param bool $removeEmpty |
---|
878 | * if true remove attribute from html tag if empty |
---|
879 | * @return string |
---|
880 | * modified tag |
---|
881 | */ |
---|
882 | protected function setTagAttribute($tag, $attribute, $value, $protect=true, $removeEmpty=false) { |
---|
883 | // preparer l'attribut |
---|
884 | // supprimer les etc mais pas les balises html |
---|
885 | // qui ont un sens dans un attribut value d'un input |
---|
886 | if ($protect) { |
---|
887 | $value = preg_replace(array(",\n,",",\s(?=\s),msS"),array(" ",""),strip_tags($value)); |
---|
888 | $value = str_replace(array("'",'"',"<",">"),array(''','"','<','>'), $value); |
---|
889 | } |
---|
890 | |
---|
891 | // echapper les ' pour eviter tout bug |
---|
892 | $value = str_replace("'", "'", $value); |
---|
893 | if ($removeEmpty AND strlen($value)==0) |
---|
894 | $insert = ''; |
---|
895 | else |
---|
896 | $insert = " $attribute='$value'"; |
---|
897 | |
---|
898 | list($old, $r) = $this->tagAttribute($tag, $attribute, true); |
---|
899 | |
---|
900 | if ($old !== NULL) { |
---|
901 | // Remplacer l'ancien attribut du meme nom |
---|
902 | $tag = $r[1].$insert.$r[5]; |
---|
903 | } |
---|
904 | else { |
---|
905 | // preferer une balise " />" (comme <img />) |
---|
906 | if (preg_match(',/>,', $tag)) |
---|
907 | $tag = preg_replace(",\s?/>,S", $insert." />", $tag, 1); |
---|
908 | // sinon une balise <a ...> ... </a> |
---|
909 | else |
---|
910 | $tag = preg_replace(",\s?>,S", $insert.">", $tag, 1); |
---|
911 | } |
---|
912 | |
---|
913 | return $tag; |
---|
914 | } |
---|
915 | |
---|
916 | /** |
---|
917 | * Provide Mime Type for Image file Extension |
---|
918 | * @param $extension |
---|
919 | * @return string |
---|
920 | */ |
---|
921 | protected function extensionToMimeType($extension){ |
---|
922 | static $MimeTable = array( |
---|
923 | 'jpg' => 'image/jpeg', |
---|
924 | 'jpeg' => 'image/jpeg', |
---|
925 | 'png' => 'image/png', |
---|
926 | 'gif' => 'image/gif', |
---|
927 | ); |
---|
928 | |
---|
929 | return (isset($MimeTable[$extension])?$MimeTable[$extension]:'image/jpeg'); |
---|
930 | } |
---|
931 | |
---|
932 | |
---|
933 | /** |
---|
934 | * Detect animated GIF : don't touch it |
---|
935 | * http://it.php.net/manual/en/function.imagecreatefromgif.php#59787 |
---|
936 | * |
---|
937 | * @param string $filename |
---|
938 | * @return bool |
---|
939 | */ |
---|
940 | protected function isAnimatedGif($filename){ |
---|
941 | $filecontents = file_get_contents($filename); |
---|
942 | |
---|
943 | $str_loc = 0; |
---|
944 | $count = 0; |
---|
945 | while ($count<2) # There is no point in continuing after we find a 2nd frame |
---|
946 | { |
---|
947 | |
---|
948 | $where1 = strpos($filecontents, "\x00\x21\xF9\x04", $str_loc); |
---|
949 | if ($where1===FALSE){ |
---|
950 | break; |
---|
951 | } else { |
---|
952 | $str_loc = $where1+1; |
---|
953 | $where2 = strpos($filecontents, "\x00\x2C", $str_loc); |
---|
954 | if ($where2===FALSE){ |
---|
955 | break; |
---|
956 | } else { |
---|
957 | if ($where1+8==$where2){ |
---|
958 | $count++; |
---|
959 | } |
---|
960 | $str_loc = $where2+1; |
---|
961 | } |
---|
962 | } |
---|
963 | } |
---|
964 | |
---|
965 | if ($count>1){ |
---|
966 | return (true); |
---|
967 | |
---|
968 | } else { |
---|
969 | return (false); |
---|
970 | } |
---|
971 | } |
---|
972 | |
---|
973 | /** |
---|
974 | * Embed image file in Base 64 URI |
---|
975 | * |
---|
976 | * @param string $filename |
---|
977 | * @param int $maxsize |
---|
978 | * @return string |
---|
979 | * URI Scheme of base64 if possible, |
---|
980 | * or URL from source file |
---|
981 | */ |
---|
982 | function base64EmbedFile ($filename, $maxsize = 32768) { |
---|
983 | $extension = substr(strrchr($filename,'.'),1); |
---|
984 | |
---|
985 | if (!file_exists($filename) |
---|
986 | OR filesize($filename)>$maxsize |
---|
987 | OR !$content = file_get_contents($filename)) |
---|
988 | return $filename; |
---|
989 | |
---|
990 | $base64 = base64_encode($content); |
---|
991 | $encoded = 'data:'.$this->extensionToMimeType($extension).';base64,'.$base64; |
---|
992 | |
---|
993 | return $encoded; |
---|
994 | } |
---|
995 | |
---|
996 | |
---|
997 | /** |
---|
998 | * Convert image to JPG and replace transparency with a background color |
---|
999 | * |
---|
1000 | * @param string $source |
---|
1001 | * source file name (or img tag) |
---|
1002 | * @param string $destDir |
---|
1003 | * destination directory |
---|
1004 | * @param string $bgColor |
---|
1005 | * hexa color |
---|
1006 | * @param int $quality |
---|
1007 | * JPG quality |
---|
1008 | * @return string |
---|
1009 | * file name of the resized image (or source image if fail) |
---|
1010 | * @throws Exception |
---|
1011 | */ |
---|
1012 | function img2JPG($source, $destDir, $bgColor='#000000', $quality=85) { |
---|
1013 | $infos = $this->readSourceImage($source, $destDir, 'jpg'); |
---|
1014 | |
---|
1015 | if (!$infos) return $source; |
---|
1016 | |
---|
1017 | $couleurs = $this->colorHEX2RGB($bgColor); |
---|
1018 | $dr= $couleurs["red"]; |
---|
1019 | $dv= $couleurs["green"]; |
---|
1020 | $db= $couleurs["blue"]; |
---|
1021 | |
---|
1022 | $srcWidth = $infos["largeur"]; |
---|
1023 | $srcHeight = $infos["hauteur"]; |
---|
1024 | |
---|
1025 | if ($infos["creer"]) { |
---|
1026 | if ($this->maxImagePxGDMemoryLimit AND $srcWidth*$srcHeight>$this->maxImagePxGDMemoryLimit){ |
---|
1027 | $this->log("No resize allowed : image is " . $srcWidth*$srcHeight . "px, larger than ".$this->maxImagePxGDMemoryLimit."px"); |
---|
1028 | return $infos["fichier"]; |
---|
1029 | } |
---|
1030 | $fonction_imagecreatefrom = $infos['fonction_imagecreatefrom']; |
---|
1031 | |
---|
1032 | if (!function_exists($fonction_imagecreatefrom)) |
---|
1033 | return $infos["fichier"]; |
---|
1034 | $im = @$fonction_imagecreatefrom($infos["fichier"]); |
---|
1035 | |
---|
1036 | if (!$im){ |
---|
1037 | throw new Exception("GD image creation fail for ".$infos["fichier"]); |
---|
1038 | } |
---|
1039 | |
---|
1040 | $this->imagepalettetotruecolor($im); |
---|
1041 | $im_ = imagecreatetruecolor($srcWidth, $srcHeight); |
---|
1042 | if ($infos["format_source"] == "gif" AND function_exists('ImageCopyResampled')) { |
---|
1043 | // if was a transparent GIF |
---|
1044 | // make a tansparent PNG |
---|
1045 | @imagealphablending($im_, false); |
---|
1046 | @imagesavealpha($im_,true); |
---|
1047 | if (function_exists("imageAntiAlias")) imageAntiAlias($im_,true); |
---|
1048 | @ImageCopyResampled($im_, $im, 0, 0, 0, 0, $srcWidth, $srcHeight, $srcWidth, $srcHeight); |
---|
1049 | imagedestroy($im); |
---|
1050 | $im = $im_; |
---|
1051 | } |
---|
1052 | |
---|
1053 | // allocate background Color |
---|
1054 | $color_t = ImageColorAllocate( $im_, $dr, $dv, $db); |
---|
1055 | |
---|
1056 | imagefill ($im_, 0, 0, $color_t); |
---|
1057 | |
---|
1058 | // JPEG has no transparency layer, no need to copy |
---|
1059 | // the image pixel by pixel |
---|
1060 | if ($infos["format_source"] == "jpg") { |
---|
1061 | $im_ = &$im; |
---|
1062 | } else |
---|
1063 | for ($x = 0; $x < $srcWidth; $x++) { |
---|
1064 | for ($y=0; $y < $srcHeight; $y++) { |
---|
1065 | |
---|
1066 | $rgb = ImageColorAt($im, $x, $y); |
---|
1067 | $a = ($rgb >> 24) & 0xFF; |
---|
1068 | $r = ($rgb >> 16) & 0xFF; |
---|
1069 | $g = ($rgb >> 8) & 0xFF; |
---|
1070 | $b = $rgb & 0xFF; |
---|
1071 | |
---|
1072 | $a = (127-$a) / 127; |
---|
1073 | |
---|
1074 | // faster if no transparency |
---|
1075 | if ($a == 1) { |
---|
1076 | $r = $r; |
---|
1077 | $g = $g; |
---|
1078 | $b = $b; |
---|
1079 | } |
---|
1080 | // faster if full transparency |
---|
1081 | else if ($a == 0) { |
---|
1082 | $r = $dr; |
---|
1083 | $g = $dv; |
---|
1084 | $b = $db; |
---|
1085 | |
---|
1086 | } |
---|
1087 | else { |
---|
1088 | $r = round($a * $r + $dr * (1-$a)); |
---|
1089 | $g = round($a * $g + $dv * (1-$a)); |
---|
1090 | $b = round($a * $b + $db * (1-$a)); |
---|
1091 | } |
---|
1092 | $a = (1-$a) *127; |
---|
1093 | $color = ImageColorAllocateAlpha( $im_, $r, $g, $b, $a); |
---|
1094 | imagesetpixel ($im_, $x, $y, $color); |
---|
1095 | } |
---|
1096 | } |
---|
1097 | if (!$this->saveGDImage($im_, $infos, $quality)){ |
---|
1098 | throw new Exception("Unable to write ".$infos['fichier_dest'].", check write right of $destDir"); |
---|
1099 | } |
---|
1100 | if ($im!==$im_) |
---|
1101 | imagedestroy($im); |
---|
1102 | imagedestroy($im_); |
---|
1103 | } |
---|
1104 | return $infos["fichier_dest"]; |
---|
1105 | } |
---|
1106 | |
---|
1107 | /** |
---|
1108 | * Resize without bluring, and save image with needed quality if JPG image |
---|
1109 | * @author : Arno* from http://zone.spip.org/trac/spip-zone/browser/_plugins_/image_responsive/action/image_responsive.php |
---|
1110 | * |
---|
1111 | * @param string $source |
---|
1112 | * @param string $dest |
---|
1113 | * @param int $maxWidth |
---|
1114 | * @param int $maxHeight |
---|
1115 | * @param int|null $quality |
---|
1116 | * @return string |
---|
1117 | * file name of the resized image (or source image if fail) |
---|
1118 | * @throws Exception |
---|
1119 | */ |
---|
1120 | function imgSharpResize($source, $dest, $maxWidth = 0, $maxHeight = 0, $quality=null){ |
---|
1121 | $infos = $this->readSourceImage($source, $dest); |
---|
1122 | if (!$infos) return $source; |
---|
1123 | |
---|
1124 | if ($maxWidth==0 AND $maxHeight==0) |
---|
1125 | return $source; |
---|
1126 | |
---|
1127 | if ($maxWidth==0) $maxWidth = 10000; |
---|
1128 | elseif ($maxHeight==0) $maxHeight = 10000; |
---|
1129 | |
---|
1130 | $srcFile = $infos['fichier']; |
---|
1131 | $srcExt = $infos['format_source']; |
---|
1132 | |
---|
1133 | $destination = dirname($infos['fichier_dest']) . "/" . basename($infos['fichier_dest'], ".".$infos["format_dest"]); |
---|
1134 | |
---|
1135 | // compute width & height |
---|
1136 | $srcWidth = $infos['largeur']; |
---|
1137 | $srcHeight = $infos['hauteur']; |
---|
1138 | list($destWidth,$destHeight) = $this->computeImageSize($srcWidth, $srcHeight, $maxWidth, $maxHeight); |
---|
1139 | |
---|
1140 | if ($infos['creer']==false) |
---|
1141 | return $infos['fichier_dest']; |
---|
1142 | |
---|
1143 | // If source image is smaller than desired size, keep source |
---|
1144 | if ($srcWidth |
---|
1145 | AND $srcWidth<=$destWidth |
---|
1146 | AND $srcHeight<=$destHeight){ |
---|
1147 | |
---|
1148 | $infos['format_dest'] = $srcExt; |
---|
1149 | $infos['fichier_dest'] = $destination.".".$srcExt; |
---|
1150 | @copy($srcFile, $infos['fichier_dest']); |
---|
1151 | |
---|
1152 | } |
---|
1153 | else { |
---|
1154 | if ($this->maxImagePxGDMemoryLimit AND $srcWidth*$srcHeight>$this->maxImagePxGDMemoryLimit){ |
---|
1155 | $this->log("No resize allowed : image is " . $srcWidth*$srcHeight . "px, larger than ".$this->maxImagePxGDMemoryLimit."px"); |
---|
1156 | return $srcFile; |
---|
1157 | } |
---|
1158 | $destExt = $infos['format_dest']; |
---|
1159 | if (!$destExt){ |
---|
1160 | throw new Exception("No output extension for {$srcFile}"); |
---|
1161 | } |
---|
1162 | |
---|
1163 | $fonction_imagecreatefrom = $infos['fonction_imagecreatefrom']; |
---|
1164 | |
---|
1165 | if (!function_exists($fonction_imagecreatefrom)) |
---|
1166 | return $srcFile; |
---|
1167 | $srcImage = @$fonction_imagecreatefrom($srcFile); |
---|
1168 | if (!$srcImage){ |
---|
1169 | throw new Exception("GD image creation fail for {$srcFile}"); |
---|
1170 | } |
---|
1171 | |
---|
1172 | // Initialization of dest image |
---|
1173 | $destImage = ImageCreateTrueColor($destWidth, $destHeight); |
---|
1174 | |
---|
1175 | // Copy and resize source image |
---|
1176 | $ok = false; |
---|
1177 | if (function_exists('ImageCopyResampled')){ |
---|
1178 | // if transparent GIF, keep the transparency |
---|
1179 | if ($srcExt=="gif"){ |
---|
1180 | $transparent_index = ImageColorTransparent($srcImage); |
---|
1181 | if($transparent_index!=(-1)){ |
---|
1182 | $transparent_color = ImageColorsForIndex($srcImage,$transparent_index); |
---|
1183 | if(!empty($transparent_color)) { |
---|
1184 | $transparent_new = ImageColorAllocate($destImage,$transparent_color['red'],$transparent_color['green'],$transparent_color['blue']); |
---|
1185 | $transparent_new_index = ImageColorTransparent($destImage,$transparent_new); |
---|
1186 | ImageFill($destImage, 0,0, $transparent_new_index); |
---|
1187 | } |
---|
1188 | } |
---|
1189 | } |
---|
1190 | if ($destExt=="png"){ |
---|
1191 | // keep transparency |
---|
1192 | if (function_exists("imageAntiAlias")) imageAntiAlias($destImage, true); |
---|
1193 | @imagealphablending($destImage, false); |
---|
1194 | @imagesavealpha($destImage, true); |
---|
1195 | } |
---|
1196 | $ok = @ImageCopyResampled($destImage, $srcImage, 0, 0, 0, 0, $destWidth, $destHeight, $srcWidth, $srcHeight); |
---|
1197 | } |
---|
1198 | if (!$ok) |
---|
1199 | $ok = ImageCopyResized($destImage, $srcImage, 0, 0, 0, 0, $destWidth, $destHeight, $srcWidth, $srcHeight); |
---|
1200 | |
---|
1201 | if ($destExt=="jpg" && function_exists('imageconvolution')){ |
---|
1202 | $intSharpness = $this->computeSharpCoeff($srcWidth, $destWidth); |
---|
1203 | $arrMatrix = array( |
---|
1204 | array(-1, -2, -1), |
---|
1205 | array(-2, $intSharpness+12, -2), |
---|
1206 | array(-1, -2, -1) |
---|
1207 | ); |
---|
1208 | imageconvolution($destImage, $arrMatrix, $intSharpness, 0); |
---|
1209 | } |
---|
1210 | // save destination image |
---|
1211 | if (!$this->saveGDImage($destImage, $infos, $quality)){ |
---|
1212 | throw new Exception("Unable to write ".$infos['fichier_dest'].", check write right of $dest"); |
---|
1213 | } |
---|
1214 | |
---|
1215 | if ($srcImage) |
---|
1216 | ImageDestroy($srcImage); |
---|
1217 | ImageDestroy($destImage); |
---|
1218 | } |
---|
1219 | |
---|
1220 | return $infos['fichier_dest']; |
---|
1221 | |
---|
1222 | } |
---|
1223 | |
---|
1224 | /** |
---|
1225 | * @author : Arno* from http://zone.spip.org/trac/spip-zone/browser/_plugins_/image_responsive/action/image_responsive.php |
---|
1226 | * |
---|
1227 | * @param int $intOrig |
---|
1228 | * @param int $intFinal |
---|
1229 | * @return mixed |
---|
1230 | */ |
---|
1231 | function computeSharpCoeff($intOrig, $intFinal) { |
---|
1232 | $intFinal = $intFinal * (750.0 / $intOrig); |
---|
1233 | $intA = 52; |
---|
1234 | $intB = -0.27810650887573124; |
---|
1235 | $intC = .00047337278106508946; |
---|
1236 | $intRes = $intA + $intB * $intFinal + $intC * $intFinal * $intFinal; |
---|
1237 | return max(round($intRes), 0); |
---|
1238 | } |
---|
1239 | |
---|
1240 | /** |
---|
1241 | * Read and preprocess informations about source image |
---|
1242 | * |
---|
1243 | * @param string $img |
---|
1244 | * HTML img tag <img src=... /> OR source filename |
---|
1245 | * @param string $dest |
---|
1246 | * Destination dir of new image |
---|
1247 | * @param null|string $outputFormat |
---|
1248 | * forced extension of output image file : jpg, png, gif |
---|
1249 | * @return bool|array |
---|
1250 | * false in case of error |
---|
1251 | * array of image information otherwise |
---|
1252 | * @throws Exception |
---|
1253 | */ |
---|
1254 | protected function readSourceImage($img, $dest, $outputFormat = null) { |
---|
1255 | if (strlen($img)==0) return false; |
---|
1256 | $ret = array(); |
---|
1257 | |
---|
1258 | $source = trim($this->tagAttribute($img, 'src')); |
---|
1259 | if (strlen($source) < 1){ |
---|
1260 | $source = $img; |
---|
1261 | $img = "<img src='$source' />"; |
---|
1262 | } |
---|
1263 | # gerer img src="data:....base64" |
---|
1264 | # don't process base64 |
---|
1265 | else if (preg_match('@^data:image/(jpe?g|png|gif);base64,(.*)$@isS', $source)) { |
---|
1266 | return false; |
---|
1267 | } |
---|
1268 | else |
---|
1269 | $source = $this->URL2filepath($source); |
---|
1270 | |
---|
1271 | // don't process distant images |
---|
1272 | if (!$source OR preg_match(';^(\w{3,7}://);', $source)){ |
---|
1273 | return false; |
---|
1274 | } |
---|
1275 | |
---|
1276 | $extension_dest = ""; |
---|
1277 | if (preg_match(",\.(gif|jpe?g|png)($|[?]),i", $source, $regs)) { |
---|
1278 | $extension = strtolower($regs[1]); |
---|
1279 | $extension_dest = $extension; |
---|
1280 | } |
---|
1281 | if (!is_null($outputFormat)) $extension_dest = $outputFormat; |
---|
1282 | |
---|
1283 | if (!$extension_dest) return false; |
---|
1284 | |
---|
1285 | if (@file_exists($source)){ |
---|
1286 | list ($ret["largeur"],$ret["hauteur"]) = $this->imgSize(strpos($img,"width=")!==false?$img:$source); |
---|
1287 | $date_src = @filemtime($source); |
---|
1288 | } |
---|
1289 | else |
---|
1290 | return false; |
---|
1291 | |
---|
1292 | // error if no known size |
---|
1293 | if (!($ret["hauteur"] OR $ret["largeur"])) |
---|
1294 | return false; |
---|
1295 | |
---|
1296 | |
---|
1297 | // dest filename : dest/md5(source) or dest if full name provided |
---|
1298 | if (substr($dest,-1)=="/"){ |
---|
1299 | $nom_fichier = md5($source); |
---|
1300 | $fichier_dest = $dest . $nom_fichier . "." . $extension_dest; |
---|
1301 | } |
---|
1302 | else |
---|
1303 | $fichier_dest = $dest; |
---|
1304 | |
---|
1305 | $creer = true; |
---|
1306 | if (@file_exists($f = $fichier_dest)){ |
---|
1307 | if (filemtime($f)>=$date_src) |
---|
1308 | $creer = false; |
---|
1309 | } |
---|
1310 | // mkdir complete path if needed |
---|
1311 | if ($creer |
---|
1312 | AND !is_dir($d=dirname($fichier_dest))){ |
---|
1313 | mkdir($d,0777,true); |
---|
1314 | if (!is_dir($d)){ |
---|
1315 | throw new Exception("Unable to mkdir {$d}"); |
---|
1316 | } |
---|
1317 | } |
---|
1318 | |
---|
1319 | $ret["fonction_imagecreatefrom"] = "imagecreatefrom".($extension != 'jpg' ? $extension : 'jpeg'); |
---|
1320 | $ret["fichier"] = $source; |
---|
1321 | $ret["fichier_dest"] = $fichier_dest; |
---|
1322 | $ret["format_source"] = ($extension != 'jpeg' ? $extension : 'jpg'); |
---|
1323 | $ret["format_dest"] = $extension_dest; |
---|
1324 | $ret["date_src"] = $date_src; |
---|
1325 | $ret["creer"] = $creer; |
---|
1326 | $ret["tag"] = $img; |
---|
1327 | |
---|
1328 | if (!function_exists($ret["fonction_imagecreatefrom"])) return false; |
---|
1329 | return $ret; |
---|
1330 | } |
---|
1331 | |
---|
1332 | /** |
---|
1333 | * Compute new image size according to max Width and max Height and initial width/height ratio |
---|
1334 | * @param int $srcWidth |
---|
1335 | * @param int $srcHeight |
---|
1336 | * @param int $maxWidth |
---|
1337 | * @param int $maxHeight |
---|
1338 | * @return array |
---|
1339 | */ |
---|
1340 | function computeImageSize($srcWidth, $srcHeight, $maxWidth, $maxHeight) { |
---|
1341 | $ratioWidth = $srcWidth/$maxWidth; |
---|
1342 | $ratioHeight = $srcHeight/$maxHeight; |
---|
1343 | |
---|
1344 | if ($ratioWidth <=1 AND $ratioHeight <=1) { |
---|
1345 | return array($srcWidth,$srcHeight); |
---|
1346 | } |
---|
1347 | else if ($ratioWidth < $ratioHeight) { |
---|
1348 | $destWidth = intval(ceil($srcWidth/$ratioHeight)); |
---|
1349 | $destHeight = $maxHeight; |
---|
1350 | } |
---|
1351 | else { |
---|
1352 | $destWidth = $maxWidth; |
---|
1353 | $destHeight = intval(ceil($srcHeight/$ratioWidth)); |
---|
1354 | } |
---|
1355 | return array ($destWidth, $destHeight); |
---|
1356 | } |
---|
1357 | |
---|
1358 | /** |
---|
1359 | * SaveAffiche ou sauvegarde une image au format PNG |
---|
1360 | * Utilise les fonctions specifiques GD. |
---|
1361 | * |
---|
1362 | * @param resource $img |
---|
1363 | * GD image resource |
---|
1364 | * @param array $infos |
---|
1365 | * image description |
---|
1366 | * @param int|null $quality |
---|
1367 | * compression quality for JPG images |
---|
1368 | * @return bool |
---|
1369 | */ |
---|
1370 | protected function saveGDImage($img, $infos, $quality=null) { |
---|
1371 | $fichier = $infos['fichier_dest']; |
---|
1372 | $tmp = $fichier.".tmp"; |
---|
1373 | switch($infos['format_dest']){ |
---|
1374 | case "gif": |
---|
1375 | $ret = imagegif($img,$tmp); |
---|
1376 | break; |
---|
1377 | case "png": |
---|
1378 | $ret = imagepng($img,$tmp); |
---|
1379 | break; |
---|
1380 | case "jpg": |
---|
1381 | case "jpeg": |
---|
1382 | $ret = imagejpeg($img,$tmp,$quality); |
---|
1383 | break; |
---|
1384 | } |
---|
1385 | if(file_exists($tmp)){ |
---|
1386 | $taille_test = getimagesize($tmp); |
---|
1387 | if ($taille_test[0] < 1) return false; |
---|
1388 | |
---|
1389 | @unlink($fichier); // le fichier peut deja exister |
---|
1390 | @rename($tmp, $fichier); |
---|
1391 | return $ret; |
---|
1392 | } |
---|
1393 | return false; |
---|
1394 | } |
---|
1395 | |
---|
1396 | |
---|
1397 | /** |
---|
1398 | * Convert indexed colors image to true color image |
---|
1399 | * available in PHP 5.5+ http://www.php.net/manual/fr/function.imagepalettetotruecolor.php |
---|
1400 | * @param resource $img |
---|
1401 | * @return bool |
---|
1402 | */ |
---|
1403 | protected function imagepalettetotruecolor(&$img) { |
---|
1404 | if (function_exists("imagepalettetotruecolor")) |
---|
1405 | return imagepalettetotruecolor($img); |
---|
1406 | |
---|
1407 | if ($img AND !imageistruecolor($img) AND function_exists('imagecreatetruecolor')) { |
---|
1408 | $w = imagesx($img); |
---|
1409 | $h = imagesy($img); |
---|
1410 | $img1 = imagecreatetruecolor($w,$h); |
---|
1411 | // keep alpha layer if possible |
---|
1412 | if(function_exists('ImageCopyResampled')) { |
---|
1413 | if (function_exists("imageAntiAlias")) imageAntiAlias($img1,true); |
---|
1414 | @imagealphablending($img1, false); |
---|
1415 | @imagesavealpha($img1,true); |
---|
1416 | @ImageCopyResampled($img1, $img, 0, 0, 0, 0, $w, $h, $w, $h); |
---|
1417 | } else { |
---|
1418 | imagecopy($img1,$img,0,0,0,0,$w,$h); |
---|
1419 | } |
---|
1420 | |
---|
1421 | $img = $img1; |
---|
1422 | return true; |
---|
1423 | } |
---|
1424 | return false; |
---|
1425 | } |
---|
1426 | |
---|
1427 | |
---|
1428 | /** |
---|
1429 | * Translate HTML color to hexa color |
---|
1430 | * @param string $color |
---|
1431 | * @return string |
---|
1432 | */ |
---|
1433 | protected function colorHTML2Hex($color){ |
---|
1434 | static $html_colors=array( |
---|
1435 | 'aqua'=>'00FFFF','black'=>'000000','blue'=>'0000FF','fuchsia'=>'FF00FF','gray'=>'808080','green'=>'008000','lime'=>'00FF00','maroon'=>'800000', |
---|
1436 | 'navy'=>'000080','olive'=>'808000','purple'=>'800080','red'=>'FF0000','silver'=>'C0C0C0','teal'=>'008080','white'=>'FFFFFF','yellow'=>'FFFF00'); |
---|
1437 | if (isset($html_colors[$lc=strtolower($color)])) |
---|
1438 | return $html_colors[$lc]; |
---|
1439 | return $color; |
---|
1440 | } |
---|
1441 | |
---|
1442 | /** |
---|
1443 | * Translate hexa color to RGB |
---|
1444 | * @param string $color |
---|
1445 | * hexa color (#000000 to #FFFFFF). |
---|
1446 | * @return array |
---|
1447 | */ |
---|
1448 | protected function colorHEX2RGB($color) { |
---|
1449 | $color = $this->colorHTML2Hex($color); |
---|
1450 | $color = ltrim($color,"#"); |
---|
1451 | $retour["red"] = hexdec(substr($color, 0, 2)); |
---|
1452 | $retour["green"] = hexdec(substr($color, 2, 2)); |
---|
1453 | $retour["blue"] = hexdec(substr($color, 4, 2)); |
---|
1454 | |
---|
1455 | return $retour; |
---|
1456 | } |
---|
1457 | |
---|
1458 | } |
---|