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  // don't resize animated gifs
214  if (self::isAnimated($imageFile)) {
215  return $imageFile;
216  }
217  // get file name and cache directory
218  $baseName = FileUtil::basename($imageFile);
219  $directory = self::getCacheDir($imageFile);
220  return self::makeRelative($directory.(strlen($width) > 0 ? $width.'-' : '').$baseName);
221  }
222 
223  /**
224  * Delete the cached images for the given image file
225  * @param $imageFile Image file located inside the upload directory of the application given as path relative to WCMF_BASE
226  */
227  public static function invalidateCache($imageFile) {
228  if (strlen($imageFile) > 0) {
229  $imageFile = URIUtil::makeRelative($imageFile, self::getMediaRootRelative());
230  $fixedFile = FileUtil::fixFilename($imageFile);
231 
232  // get file name and cache directory
233  $baseName = FileUtil::basename($imageFile);
234  $directory = self::getCacheDir($imageFile);
235 
236  // delete matches of the form ([0-9]+)-$fixedFile
237  if (is_dir($directory)) {
238  foreach (FileUtil::getFiles($directory) as $file) {
239  $matches = [];
240  if (preg_match('/^([0-9]+)-/', $file, $matches) && $matches[1].'-'.$baseName === $file) {
241  unlink($directory.$file);
242  }
243  }
244  }
245  }
246  }
247 
248  /**
249  * Get the cache directory for the given source image file
250  * @param $imageFile
251  * @return String
252  */
253  private static function getCacheDir($imageFile) {
254  $mediaRoot = self::getMediaRootRelative();
255  return self::getCacheRoot().dirname(substr($imageFile, strlen($mediaRoot))).'/';
256  }
257 
258  /**
259  * Get the source directory for the given cached image location
260  * @param $location
261  * @return String
262  */
263  private static function getSourceDir($location) {
264  return self::getMediaRootRelative().dirname($location).'/';
265  }
266 
267  /**
268  * Get the absolute image cache root directory
269  * @return String
270  */
271  private static function getCacheRoot() {
272  $config = ObjectFactory::getInstance('configuration');
273  return $config->getDirectoryValue('cacheDir', 'FrontendCache').self::IMAGE_CACHE_SECTION.'/';
274  }
275 
276  /**
277  * Get the media root directory relative to the executed script
278  * @return String
279  */
280  private static function getMediaRootRelative() {
281  $config = ObjectFactory::getInstance('configuration');
282  $mediaRootAbs = $config->getDirectoryValue('uploadDir', 'Media');
283  return self::makeRelative($mediaRootAbs);
284  }
285 
286  /**
287  * Make the current location relative to the executed script
288  * @param $location
289  * @return String
290  */
291  private static function makeRelative($location) {
292  if (self::$scriptDirAbs == null) {
293  self::$scriptDirAbs = dirname(FileUtil::realpath($_SERVER['SCRIPT_FILENAME'])).'/';
294  }
295  return URIUtil::makeRelative($location, self::$scriptDirAbs);
296  }
297 
298  /**
299  * Resize the given image to the given width
300  * @param $sourceFile
301  * @param $destFile
302  * @param $width
303  */
304  private static function resizeImage($sourceFile, $destFile, $width) {
305  $image = new \Gumlet\ImageResize($sourceFile);
306  $image->resizeToWidth($width);
307  $image->save($destFile);
308  }
309 
310  /**
311  * Check if an image file is animated
312  * @param $imageFile
313  * @return boolean
314  */
315  private static function isAnimated($imageFile) {
316  if (!($fh = @fopen($imageFile, 'rb'))) {
317  return false;
318  }
319  $count = 0;
320  //an animated gif contains multiple "frames", with each frame having a
321  //header made up of:
322  // * a static 4-byte sequence (\x00\x21\xF9\x04)
323  // * 4 variable bytes
324  // * a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?)
325 
326  // We read through the file til we reach the end of the file, or we've found
327  // at least 2 frame headers
328  while (!feof($fh) && $count < 2) {
329  $chunk = fread($fh, 1024 * 100); //read 100kb at a time
330  $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches);
331  }
332 
333  fclose($fh);
334  return $count > 1;
335  }
336 }
337 ?>
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:227
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