ImageUtil.php
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2020 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of the MIT License.
7  *
8  * See the LICENSE file distributed with this work for
9  * additional information.
10  */
11 namespace wcmf\lib\io;
12 
16 
17 if (!class_exists('\Gumlet\ImageResize')) {
18  throw new ConfigurationException(
19  'ImageUtil requires ImageResize to resize images. '.
20  'If you are using composer, add gumlet/php-image-resize '.
21  'as dependency to your project');
22 }
23 
24 /**
25  * ImageUtil provides support for image handling.
26  *
27  * @author ingo herwig <ingo@wemove.com>
28  */
29 class ImageUtil {
30 
31  const IMAGE_CACHE_SECTION = 'images';
32 
33  private static $scriptDirAbs = null;
34 
35  /**
36  * Create an HTML image tag using srcset and sizes attributes. The original image is supposed
37  * to be located inside the upload directory of the application (_Media_ configuration section).
38  * The image locations in the srcset attribute will point to the frontend cache directory
39  * (_FrontendCache_ configuration section).
40  * @param $imageFile The image file location relative to the upload directory
41  * @param $widths Array of sorted width values to be used in the srcset attribute
42  * @param $type Indicates how width values should be used (optional, default: w)
43  * - w: Values will be used as pixels, e.g. widths="1600,960" results in srcset="... 1600w, ... 960w"
44  * - x: Values will be used as pixel ration, e.g. widths="1600,960" results in srcset="... 2x, ... 1x"
45  * @param $sizes String of media queries to define image size in relation of the viewport (optional)
46  * @param $useDataAttributes Boolean indicating whether to replace src, srcset, sizes by data-src, data-srcset, data-sizes (optional, default: __false__)
47  * @param $alt Alternative text (optional)
48  * @param $class Image class (optional)
49  * @param $title Image title (optional)
50  * @param $data Data attributes as key/value pairs
51  * @param $width Width in pixels to output for the width attribute, the height attribute will be calculated according to the aspect ration (optional)
52  * @param $fallbackFile The image file to use, if imageFile does not exist (optional)
53  * @param $generate Boolean indicating whether to generate the images or not (optional, default: __false__)
54  * @return String
55  */
56  public static function getImageTag($imageFile, $widths, $type='w', $sizes='',
57  $useDataAttributes=false, $alt='', $class='', $title='', array $data=[], $width=null, $fallbackFile='',
58  $generate=false) {
59  // check if the image files exist
60  if (!FileUtil::fileExists($imageFile)) {
61  // try the fallback
62  $imageFile = $fallbackFile;
63  if (!FileUtil::fileExists($imageFile)) {
64  return '';
65  }
66  }
67 
68  $srcset = [];
69 
70  // don't resize animated gifs
71  $isAnimated = self::isAnimated($imageFile);
72  if (!$isAnimated) {
73  $fixedFile = FileUtil::fixFilename($imageFile);
74 
75  // get the image size in order to see if we have to resize
76  $imageInfo = getimagesize($fixedFile);
77  if ($imageInfo == false) {
78  // the file is no image
79  return '';
80  }
81 
82  // create src entries
83  $hasSrcSet = sizeof($widths) > 0;
84  $widths = $hasSrcSet ? $widths : [$width];
85 
86  // skip processing for fallback image
87  if ($imageFile != $fallbackFile) {
88  // get file name and cache directory
89  $baseName = FileUtil::basename($imageFile);
90  $directory = self::getCacheDir($imageFile);
91 
92  // create the cache directory if requested
93  if ($generate) {
94  FileUtil::mkdirRec($directory);
95  }
96 
97  for ($i=0, $count=sizeof($widths); $i<$count; $i++) {
98  $curWidth = intval($widths[$i]);
99  if ($curWidth > 0) {
100  $resizedFile = self::makeRelative($directory.$curWidth.'-'.$baseName);
101 
102  // create the cached file if requested
103  if ($generate) {
104  // only if the requested width is smaller than the image width
105  if ($curWidth < $imageInfo[0]) {
106  // if the file does not exist in the cache or is older
107  // than the source file, we create it
108  $dateOrig = @filemtime($fixedFile);
109  $dateCache = @filemtime($resizedFile);
110  if (!file_exists($resizedFile) || $dateOrig > $dateCache) {
111  self::resizeImage($fixedFile, $resizedFile, $curWidth);
112  }
113 
114  // fallback to source file, if cached file could not be created
115  if (!file_exists($resizedFile)) {
116  $resizedFile = $imageFile;
117  }
118  }
119  }
120 
121  if ($hasSrcSet) {
122  // add to source set
123  $srcset[] = FileUtil::urlencodeFilename($resizedFile).' '.($type === 'w' ? $curWidth.'w' : ($count-$i).'x');
124  }
125  else {
126  // replace main source for single source entry
127  $imageFile = $resizedFile;
128  }
129  }
130  }
131  }
132  }
133 
134  $tag = '<img '.($useDataAttributes ? 'data-' : '').'src="'.FileUtil::urlencodeFilename($imageFile).'" alt="'.$alt.'"'.
135  (strlen($class) > 0 ? ' class="'.$class.'"' : '').
136  (strlen($title) > 0 ? ' title="'.$title.'"' : '');
137  foreach ($data as $name => $value) {
138  $tag .= ' data-'.$name.'="'.str_replace('"', '\"', $value).'"';
139  }
140  if (sizeof($srcset) > 0) {
141  $tag .= ' '.($useDataAttributes ? 'data-' : '').'srcset="'.join(', ', $srcset).'"'.
142  ' '.(strlen($sizes) > 0 ? ($useDataAttributes ? 'data-' : '').'sizes="'.$sizes.'"' : '');
143  }
144  if ($width != null) {
145  $width = intval($width);
146  $height = intval($width * $imageInfo[1] / $imageInfo[0]);
147  $tag .= ' width="'.$width.'" height="'.$height.'"';
148  }
149  $tag = trim($tag).'>';
150  return $tag;
151  }
152 
153  /**
154  * Output the cached image for the given cache location
155  * @param $location
156  * @param $returnLocation Boolean indicating if only the file location should be returned (optional)
157  * @param $callback Function called, after the cached image is created, receives the original and cached image as parameters (optional)
158  * @return String, if returnLocation is true
159  */
160  public static function getCachedImage($location, $returnLocation=false, $callback=null) {
161  $location = rawurldecode($location);
162 
163  // strip the cache base from the location
164  $cacheLocation = substr($location, strlen(self::IMAGE_CACHE_SECTION.'/'));
165 
166  // determine the width and source file from the location
167  // the location is supposed to follow the pattern directory/{width}-basename
168  $width = null;
169  $basename = FileUtil::basename($cacheLocation);
170  if (preg_match('/^([0-9]+)-/', $basename, $matches)) {
171  // get required width from location and remove it from location
172  $width = $matches[1];
173  $basename = preg_replace('/^'.$width.'-/', '', $basename);
174  }
175  $sourceFile = self::getSourceDir($cacheLocation).$basename;
176 
177  // create the resized image file, if not existing
178  $resizedFile = self::getCacheRoot().$cacheLocation;
179  if (FileUtil::fileExists($sourceFile) && !FileUtil::fileExists($resizedFile)) {
180  FileUtil::mkdirRec(pathinfo($resizedFile, PATHINFO_DIRNAME));
181  $fixedFile = FileUtil::fixFilename($sourceFile);
182  if ($width !== null) {
183  self::resizeImage($fixedFile, $resizedFile, $width);
184  }
185  else {
186  // just copy in case of undefined width
187  copy($fixedFile, $resizedFile);
188  }
189  if (is_callable($callback)) {
190  $callback($fixedFile, $resizedFile);
191  }
192  }
193 
194  // return the image file
195  $file = FileUtil::fileExists($resizedFile) ? $resizedFile : (FileUtil::fileExists($sourceFile) ? $sourceFile : null);
196  if ($returnLocation) {
197  return $file;
198  }
199  $imageInfo = getimagesize($file);
200  $image = file_get_contents($file);
201  header('Content-type: '.$imageInfo['mime'].';');
202  header("Content-Length: ".strlen($image));
203  echo $image;
204  }
205 
206  /**
207  * Get the cache location for the given image and width
208  * @param $imageFile Image file located inside the upload directory of the application given as path relative to WCMF_BASE
209  * @param $width
210  * @return String
211  */
212  public static function getCacheLocation($imageFile, $width) {
213  // get file name and cache directory
214  $baseName = FileUtil::basename($imageFile);
215  $directory = self::getCacheDir($imageFile);
216  return self::makeRelative($directory.(strlen($width) > 0 ? $width.'-' : '').$baseName);
217  }
218 
219  /**
220  * Delete the cached images for the given image file
221  * @param $imageFile Image file located inside the upload directory of the application given as path relative to WCMF_BASE
222  */
223  public static function invalidateCache($imageFile) {
224  if (strlen($imageFile) > 0) {
225  $imageFile = URIUtil::makeRelative($imageFile, self::getMediaRootRelative());
226  $fixedFile = FileUtil::fixFilename($imageFile);
227 
228  // get file name and cache directory
229  $baseName = FileUtil::basename($imageFile);
230  $directory = self::getCacheDir($imageFile);
231 
232  // delete matches of the form ([0-9]+)-$fixedFile
233  if (is_dir($directory)) {
234  foreach (FileUtil::getFiles($directory) as $file) {
235  $matches = [];
236  if (preg_match('/^([0-9]+)-/', $file, $matches) && $matches[1].'-'.$baseName === $file) {
237  unlink($directory.$file);
238  }
239  }
240  }
241  }
242  }
243 
244  /**
245  * Get the cache directory for the given source image file
246  * @param $imageFile
247  * @return String
248  */
249  private static function getCacheDir($imageFile) {
250  $mediaRoot = self::getMediaRootRelative();
251  return self::getCacheRoot().dirname(substr($imageFile, strlen($mediaRoot))).'/';
252  }
253 
254  /**
255  * Get the source directory for the given cached image location
256  * @param $location
257  * @return String
258  */
259  private static function getSourceDir($location) {
260  return self::getMediaRootRelative().dirname($location).'/';
261  }
262 
263  /**
264  * Get the absolute image cache root directory
265  * @return String
266  */
267  private static function getCacheRoot() {
268  $config = ObjectFactory::getInstance('configuration');
269  return $config->getDirectoryValue('cacheDir', 'FrontendCache').self::IMAGE_CACHE_SECTION.'/';
270  }
271 
272  /**
273  * Get the media root directory relative to the executed script
274  * @return String
275  */
276  private static function getMediaRootRelative() {
277  $config = ObjectFactory::getInstance('configuration');
278  $mediaRootAbs = $config->getDirectoryValue('uploadDir', 'Media');
279  return self::makeRelative($mediaRootAbs);
280  }
281 
282  /**
283  * Make the current location relative to the executed script
284  * @param $location
285  * @return String
286  */
287  private static function makeRelative($location) {
288  if (self::$scriptDirAbs == null) {
289  self::$scriptDirAbs = dirname(FileUtil::realpath($_SERVER['SCRIPT_FILENAME'])).'/';
290  }
291  return URIUtil::makeRelative($location, self::$scriptDirAbs);
292  }
293 
294  /**
295  * Resize the given image to the given width
296  * @param $sourceFile
297  * @param $destFile
298  * @param $width
299  */
300  private static function resizeImage($sourceFile, $destFile, $width) {
301  $image = new \Gumlet\ImageResize($sourceFile);
302  $image->resizeToWidth($width);
303  $image->save($destFile);
304  }
305 
306  /**
307  * Check if an image file is animated
308  * @param $imageFile
309  * @return boolean
310  */
311  private static function isAnimated($imageFile) {
312  if (!($fh = @fopen($imageFile, 'rb'))) {
313  return false;
314  }
315  $count = 0;
316  //an animated gif contains multiple "frames", with each frame having a
317  //header made up of:
318  // * a static 4-byte sequence (\x00\x21\xF9\x04)
319  // * 4 variable bytes
320  // * a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?)
321 
322  // We read through the file til we reach the end of the file, or we've found
323  // at least 2 frame headers
324  while (!feof($fh) && $count < 2) {
325  $chunk = fread($fh, 1024 * 100); //read 100kb at a time
326  $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
327  }
328 
329  fclose($fh);
330  return $count > 1;
331  }
332 }
333 ?>
static getCachedImage($location, $returnLocation=false, $callback=null)
Output the cached image for the given cache location.
Definition: ImageUtil.php:160
static mkdirRec($dirname, $perm=0775)
Recursive directory creation.
Definition: FileUtil.php:215
static getFiles($directory, $pattern='/./', $prependDirectoryName=false, $recursive=false)
Definition: FileUtil.php:98
Input/Output related interfaces and classes.
Definition: namespaces.php:21
static getCacheLocation($imageFile, $width)
Get the cache location for the given image and width.
Definition: ImageUtil.php:212
static basename($file)
Get the trailing name component of a path (locale independent)
Definition: FileUtil.php:327
static fileExists($file)
Check if the given file exists.
Definition: FileUtil.php:318
static getImageTag($imageFile, $widths, $type='w', $sizes='', $useDataAttributes=false, $alt='', $class='', $title='', array $data=[], $width=null, $fallbackFile='', $generate=false)
Create an HTML image tag using srcset and sizes attributes.
Definition: ImageUtil.php:56
ConfigurationException signals an exception in the configuration.
static invalidateCache($imageFile)
Delete the cached images for the given image file.
Definition: ImageUtil.php:223
ImageUtil provides support for image handling.
Definition: ImageUtil.php:29
URIUtil provides support for uri manipulation.
Definition: URIUtil.php:18
static getInstance($name, $dynamicConfiguration=[])
static realpath($path)
Realpath function that also works for non existing paths code from http://www.php....
Definition: FileUtil.php:244
ObjectFactory implements the service locator pattern by wrapping a Factory instance and providing sta...
static fixFilename($file)
Fix the name of an existing file to be used with php file functions.
Definition: FileUtil.php:286
static urlencodeFilename($file)
Url encode a file path.
Definition: FileUtil.php:304
static makeRelative($absUri, $base)
Convert an absolute URI to a relative code from http://www.webmasterworld.com/forum88/334....
Definition: URIUtil.php:27