ImageStorage::createFilePath()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 1
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Rostenkowski\Resize;
4
5
6
use Nette\Application\Responses\FileResponse;
7
use Nette\Http\FileUpload;
8
use Nette\Utils\Image;
9
use Nette\Utils\Strings;
10
use Rostenkowski\Resize\Directory\Directory;
11
use Rostenkowski\Resize\Entity\EmptyImage;
12
use Rostenkowski\Resize\Entity\ImageEnvelope;
0 ignored issues
show
Bug introduced by
The type Rostenkowski\Resize\Entity\ImageEnvelope was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use Rostenkowski\Resize\Exceptions\ImageTypeException;
14
use Rostenkowski\Resize\Exceptions\InvalidCacheDirectoryException;
15
use Rostenkowski\Resize\Exceptions\UploaderException;
16
use Rostenkowski\Resize\Files\ImageFile;
17
18
/**
19
 * Image file storage
20
 *
21
 * - The images are stored in a regular files in the given `$directory`.
22
 * - The files are organized in a 2-level directory structure with maximum of 256² directories.
23
 * - The directory tree is well balanced thanks to the image hashes used for the directory path creation.
24
 * - The storage stores only one file even if the same image is stored multiple times, thus images should be
25
 *   deleted only after it is sure it is not referenced from other entities.
26
 * - The image thumbnails are created on demand and cached in the `$cacheDirectory`.
27
 */
28
class ImageStorage implements Storage
29
{
30
31
	/**
32
	 * The directory to store the images in
33
	 *
34
	 * @var Directory
35
	 */
36
	private $directory;
37
38
	/**
39
	 * The image type -> file extension map
40
	 *
41
	 * @var array
42
	 */
43
	private $extensions = array(Image::JPEG => 'jpg', Image::PNG => 'png', Image::GIF => 'gif');
44
45
	/**
46
	 * The image type -> MIME type map
47
	 *
48
	 * @var array
49
	 */
50
	private $mimeTypes = array(Image::JPEG => 'image/jpeg', Image::PNG => 'image/png', Image::GIF => 'image/gif');
51
52
	/**
53
	 * The public accessible URL of the cache directory
54
	 *
55
	 * @var string
56
	 */
57
	private $baseUrl;
58
59
	/**
60
	 * The cache directory
61
	 *
62
	 * @var Directory
63
	 */
64
	private $cacheDirectory;
65
66
67
	/**
68
	 * Constructs the image file storage from the given arguments.
69
	 *
70
	 * @param string  $directory The directory to store the images in
71
	 * @param string  $cacheDirectory
72
	 * @param string  $baseUrl
73
	 * @param boolean $tryCreateDirectories
74
	 */
75
	public function __construct($directory, $cacheDirectory, $baseUrl = '/cache/', $tryCreateDirectories = true)
76
	{
77
		$this->directory = new Directory($directory, $tryCreateDirectories);
78
		$this->cacheDirectory = new Directory($cacheDirectory, $tryCreateDirectories);
79
80
		if ($this->directory->is($this->cacheDirectory)) {
81
			throw new InvalidCacheDirectoryException($cacheDirectory);
82
		}
83
84
		$this->setBaseUrl($baseUrl);
85
	}
86
87
88
	/**
89
	 * Fetches the cached copy of an image by given request.
90
	 *
91
	 * @param  Request $request The image request
92
	 * @return Image
93
	 */
94
	public function fetch(Request $request)
95
	{
96
		$this->checkImage($request);
97
		$filename = $this->createCacheFilename($request);
98
99
		return Image::fromFile($filename);
100
	}
101
102
103
	/**
104
	 * Fetches the original image by the given image meta information.
105
	 *
106
	 * @param  Meta $meta The stored image meta information
107
	 * @return Image The stored image
108
	 */
109
	public function original(Meta $meta)
110
	{
111
		return Image::fromFile($this->createFilename($meta));
0 ignored issues
show
Bug Best Practice introduced by
The expression return Nette\Utils\Image...>createFilename($meta)) returns the type Nette\Utils\Image which is incompatible with the return type mandated by Rostenkowski\Resize\Storage::original() of Rostenkowski\Resize\Image.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
112
	}
113
114
115
	/**
116
	 * Removes the image from the storage by the given image meta information.
117
	 *
118
	 * @param  Meta $meta The image information
119
	 * @return ImageStorage Fluent interface
120
	 */
121
	public function remove(Meta $meta)
122
	{
123
		unlink($this->createFilename($meta));
124
125
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Rostenkowski\Resize\ImageStorage which is incompatible with the return type mandated by Rostenkowski\Resize\Storage::remove() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
126
	}
127
128
129
	/**
130
	 * Checks if an image of the given meta information is stored in the storage.
131
	 *
132
	 * @param  Meta $meta The image meta information
133
	 * @return boolean TRUE if image is present in the storage or FALSE otherwise
134
	 */
135
	public function contains(Meta $meta)
136
	{
137
		return file_exists($this->createFilename($meta));
138
	}
139
140
141
	/**
142
	 * Stores the given uploaded file.
143
	 *
144
	 * @param  FileUpload $upload
145
	 * @param  meta       $meta
146
	 * @return ImageStorage Fluent interface
147
	 * @throws UploaderException
148
	 */
149
	public function upload(FileUpload $upload, Meta $meta)
150
	{
151
		if ($upload->getError()) {
152
			throw new UploaderException($upload->getError());
153
		}
154
155
		$source = $upload->getTemporaryFile();
156
		$this->readMeta($source, $meta);
157
		$target = $this->createFilename($meta);
158
159
		if (!$this->contains($meta)) {
160
			$image = Image::fromFile($source);
161
			$image->resize(1920, 1080);
162
			$image->save($target);
163
		}
164
		if (file_exists($source)) {
165
166
			unlink($source);
167
		}
168
169
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Rostenkowski\Resize\ImageStorage which is incompatible with the return type mandated by Rostenkowski\Resize\Storage::upload() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
170
	}
171
172
173
	/**
174
	 * Returns the URL of the cached version of the image.
175
	 *
176
	 * @param  Request $request The image request
177
	 * @return string  The URL of the image
178
	 */
179
	public function link(Request $request)
180
	{
181
		$this->checkImage($request);
182
		$filePath = $this->createFilePath($request);
183
184
		return "$this->baseUrl$filePath";
185
	}
186
187
188
	/**
189
	 * Creates the file download HTTP response which can be easily sent using the `send()` method.
190
	 *
191
	 * @param  Request $request The image request
192
	 * @return FileResponse
193
	 */
194
	public function download(Request $request)
195
	{
196
		$this->checkImage($request);
197
		$filename = $this->createCacheFilename($request);
198
199
		return new FileResponse($filename, basename($filename), $request->getMeta()->getType());
0 ignored issues
show
Bug Best Practice introduced by
The expression return new Nette\Applica...->getMeta()->getType()) returns the type Nette\Application\Responses\FileResponse which is incompatible with the return type mandated by Rostenkowski\Resize\Storage::download() of Rostenkowski\Resize\IResponse.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
200
	}
201
202
203
	/**
204
	 * Renders the image directly to the standard output.
205
	 *
206
	 * @param  Request $request The image request
207
	 * @return ImageStorage Fluent interface
208
	 */
209
	public function send(Request $request)
210
	{
211
		$this->checkImage($request);
212
		$filename = $this->createCacheFilename($request);
213
		$fp = fopen($filename, 'r');
214
		header("Content-type: " . $this->getMimeType($request->getMeta()->getType()));
215
		fpassthru($fp);
216
		fclose($fp);
217
218
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Rostenkowski\Resize\ImageStorage which is incompatible with the return type mandated by Rostenkowski\Resize\Storage::send() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
219
	}
220
221
222
	/**
223
	 * Checks that the cached version of the image exists and creates it if not.
224
	 *
225
	 * @param  Request $request The image request
226
	 * @return string  The file name of the existing cached version of an image
227
	 */
228
	private function checkImage(Request $request)
229
	{
230
		$filename = $this->createCacheFilename($request);
231
232
		if (!file_exists($filename)) {
233
234
			$meta = $request->getMeta();
235
236
			if (!$this->contains($meta)) {
237
238
				// Use another meta
239
				$meta = new EmptyImage();
240
				$request->setMeta($meta);
241
				$filename = $this->createCacheFilename($request);
242
243
			}
244
245
			$original = $this->original($meta);
246
247
			if ($request->getCrop()) {
248
				$image = $this->crop($original, $request);
249
			} else {
250
				$image = $this->resize($original, $request);
251
			}
252
			new Directory(dirname($filename));
253
			$image->save($filename, 75, $meta->getType());
254
		}
255
	}
256
257
258
	/**
259
	 * Returns the file name of the cached version of the image.
260
	 *
261
	 * @param Request $request
262
	 * @return string The file name of the cached version of the image
263
	 */
264
	private function createCacheFilename(Request $request)
265
	{
266
		$filePath = $this->createFilePath($request);
267
268
		return "$this->cacheDirectory/$filePath";
269
	}
270
271
272
	/**
273
	 * Crops the given image using the given image request options.
274
	 *
275
	 * @param  Image   $image   The image to resize
276
	 * @param  Request $request The image request
277
	 * @return Image   The image thumbnail
278
	 */
279
	private function crop(Image $image, Request $request)
280
	{
281
		if ($request->getDimensions() === Request::ORIGINAL) {
282
283
			return $image;
284
		}
285
286
		list($width, $height) = $this->processDimensions($request->getDimensions());
287
288
		$resizeWidth = $width;
289
		$resizeHeight = $height;
290
291
		$originalWidth = $request->getMeta()->getWidth();
0 ignored issues
show
Bug introduced by
The method getWidth() does not exist on Rostenkowski\Resize\Meta. Since it exists in all sub-types, consider adding an abstract or default implementation to Rostenkowski\Resize\Meta. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

291
		$originalWidth = $request->getMeta()->/** @scrutinizer ignore-call */ getWidth();
Loading history...
292
		$originalHeight = $request->getMeta()->getHeight();
0 ignored issues
show
Bug introduced by
The method getHeight() does not exist on Rostenkowski\Resize\Meta. Since it exists in all sub-types, consider adding an abstract or default implementation to Rostenkowski\Resize\Meta. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

292
		$originalHeight = $request->getMeta()->/** @scrutinizer ignore-call */ getHeight();
Loading history...
293
		$originalLandscape = $originalWidth > $originalHeight;
294
295
		$cropLandscape = $width > $height;
296
		$equals = $width === $height;
297
298
		if ($originalLandscape) {
299
300
			if ($cropLandscape) {
301
302
				$coefficient = $originalHeight / $height;
303
				$scaledWidth = round($originalWidth / $coefficient);
304
305
				$left = round(($scaledWidth - $width) / 2);
306
				$top = 0;
307
308 View Code Duplication
				if ($scaledWidth < $width) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
309
					$coefficient = $originalWidth / $width;
310
					$scaledHeight = round($originalHeight / $coefficient);
311
312
					$left = 0;
313
					$top = round(($scaledHeight - $height) / 2);
314
				}
315
316 View Code Duplication
			} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
317
318
				$coefficient = $originalHeight / $height;
319
				$scaledWidth = round($originalWidth / $coefficient);
320
321
				$left = round(($scaledWidth - $width) / 2);
322
				$top = 0;
323
			}
324
325
		} else {
326
327
			if ($cropLandscape || $equals) {
328
329
				$coefficient = $originalWidth / $width;
330
				$scaledHeight = round($originalHeight / $coefficient);
331
332
				$left = 0;
333
				$top = round(($scaledHeight - $height) / 2);
334
335 View Code Duplication
			} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
336
337
				$coefficient = $originalHeight / $height;
338
				$scaledWidth = round($originalWidth / $coefficient);
339
340
				$left = round(($scaledWidth - $width) / 2);
341
				$top = 0;
342
343
			}
344
		}
345
346
		$image->resize($resizeWidth, $resizeHeight, Image::FILL);
347
		$image->crop($left, $top, $width, $height);
348
349
		return $image;
350
	}
351
352
353
	/**
354
	 * Resizes the given image to the given dimensions using given flags.
355
	 *
356
	 * @param  Image   $image   The image to resize
357
	 * @param  Request $request The image request
358
	 * @return Image   The image thumbnail
359
	 */
360
	private function resize(Image $image, Request $request)
361
	{
362
		if ($request->getDimensions() === Request::ORIGINAL) {
363
364
			return $image;
365
		}
366
367
		list($width, $height) = $this->processDimensions($request->getDimensions());
368
369
		return $image->resize($width, $height, $request->getFlags());
370
	}
371
372
373
	/**
374
	 * Returns the part of the image filename relative to the cache directory.
375
	 *
376
	 * @param  Request $request The image request
377
	 * @return string
378
	 */
379
	private function createFilePath(Request $request)
380
	{
381
		$dimensions = $request->getDimensions();
382
		$flags = $request->getFlags();
383
		$crop = (int) $request->getCrop();
384
385
		$meta = $request->getMeta();
386
		$ext = $this->getExtension($meta->getType());
387
		$baseFilePath = $this->createCacheDirectoryPath($meta->getHash());
388
389
		return "$baseFilePath.$dimensions.$flags.$crop.$ext";
390
	}
391
392
393
	/**
394
	 * Parses the given dimensions string for the image width and height.
395
	 *
396
	 * @param  string $dimensions The dimensions string
397
	 * @return array  The width and height of the image in pixels
398
	 */
399
	private function processDimensions($dimensions)
400
	{
401
		if (strpos($dimensions, 'x') !== false) {
402
			list($width, $height) = explode('x', $dimensions); // different dimensions, eg. "210x150"
403
			$width = intval($width);
404
			$height = intval($height);
405
		} else {
406
			$width = intval($dimensions); // same dimensions, eg. "210" => 210x210
407
			$height = $width;
408
		}
409
410
		return array($width, $height);
411
	}
412
413
414
	/**
415
	 * Returns the file extension for the given image type.
416
	 *
417
	 * @param  integer $type The image type
418
	 * @return string             The file extension
419
	 * @throws ImageTypeException
420
	 */
421
	private function getExtension($type)
422
	{
423
		if (!$type || !key_exists($type, $this->extensions)) {
424
425
			// SUDDEN DEATH
426
			throw new \Exception('images: get extension for type ' . var_export($type, true) . ' not found');
427
		}
428
429
		return $this->extensions[$type];
430
	}
431
432
433
	/**
434
	 * Creates the internal directory path from the given hash.
435
	 *
436
	 * Some special images like the "Image Not Available" image are stored
437
	 * in directories prefixed with an underscore. Those directories are not
438
	 * fragmented to hash based structure.
439
	 *
440
	 * @param  string $hash Tha SHA1 hash
441
	 * @return string
442
	 */
443
	private function createCacheDirectoryPath($hash)
444
	{
445
		if ($hash{0} === '_') {
446
			return "$hash/$hash";
447
		}
448
449
		return "$hash[0]$hash[1]/$hash[2]$hash[3]" . '/' . $hash;
450
	}
451
452
453
	/**
454
	 * Creates the absolute file name from the given meta information.
455
	 *
456
	 * @param  Meta $meta The image meta information
457
	 * @return string The absolute file name
458
	 */
459
	private function createFilename(Meta $meta)
460
	{
461
		$path = $this->createDirectoryPath($meta->getHash());
462
		$ext = $this->getExtension($meta->getType());
463
		$hash = $meta->getHash();
464
465
		$filename = "$this->directory/$path/$hash.$ext";
466
467
		new Directory(dirname($filename));
468
469
		return $filename;
470
	}
471
472
473
	/**
474
	 * Creates the internal directory path from the given hash.
475
	 *
476
	 * Some special images like the "Image Not Available" image are stored
477
	 * in directories prefixed with an underscore. Those directories are not
478
	 * fragmented to hash based structure.
479
	 *
480
	 * @param  string $hash Tha SHA1 hash
481
	 * @return string
482
	 */
483
	private function createDirectoryPath($hash)
484
	{
485
		if ($hash{0} === '_') {
486
			return $hash;
487
		}
488
489
		return "$hash[0]$hash[1]/$hash[2]$hash[3]";
490
	}
491
492
493
	/**
494
	 * Reads the image type from the given `$filename`, computes the image hash
495
	 * and store these information in the given `$meta`.
496
	 *
497
	 * @param string $filename The file to read the meta-data from
498
	 * @param Meta   $meta     The object to store meta-data in
499
	 */
500
	private function readMeta($filename, Meta $meta)
501
	{
502
		$info = getimagesize($filename);
503
		$meta->setHash($this->hash($filename));
504
		$meta->setWidth($info[0]);
0 ignored issues
show
Bug introduced by
The method setWidth() does not exist on Rostenkowski\Resize\Meta. It seems like you code against a sub-type of Rostenkowski\Resize\Meta such as Rostenkowski\Resize\Entity\ImageEntity. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

504
		$meta->/** @scrutinizer ignore-call */ setWidth($info[0]);
Loading history...
505
		$meta->setHeight($info[1]);
0 ignored issues
show
Bug introduced by
The method setHeight() does not exist on Rostenkowski\Resize\Meta. It seems like you code against a sub-type of Rostenkowski\Resize\Meta such as Rostenkowski\Resize\Entity\ImageEntity. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

505
		$meta->/** @scrutinizer ignore-call */ setHeight($info[1]);
Loading history...
506
		$meta->setType($info[2]);
507
	}
508
509
510
	/**
511
	 * Computes the SHA1 hash for the given file.
512
	 *
513
	 * @param string $filename The file to compute the hash from
514
	 * @return string The SHA1 hash of a file
515
	 */
516
	private function hash($filename)
517
	{
518
		return sha1_file($filename);
519
	}
520
521
522
	/**
523
	 * Returns the image MIME type according to the given image type.
524
	 *
525
	 * @param integer $imageType Image type
526
	 * @return string
527
	 */
528
	protected function getMimeType($imageType)
529
	{
530
		return $this->mimeTypes[$imageType];
531
	}
532
533
534
	/**
535
	 * Returns the original stored image filename.
536
	 *
537
	 * @param Meta $meta Image meta information
538
	 * @return string
539
	 */
540
	public function file(Meta $meta)
541
	{
542
		return $this->createFilename($meta);
543
	}
544
545
546
	/**
547
	 * Returns the public accessible cache directory URL.
548
	 *
549
	 * @return string
550
	 */
551
	public function getBaseUrl()
552
	{
553
		return $this->baseUrl;
554
	}
555
556
557
	/**
558
	 * Sets the public accessible cache directory URL.
559
	 *
560
	 * @param string $baseUrl
561
	 * @return $this
562
	 */
563
	protected function setBaseUrl($baseUrl)
564
	{
565
		if (!Strings::endsWith($baseUrl, '/')) {
566
			$baseUrl .= '/';
567
		}
568
		$this->baseUrl = $baseUrl;
569
570
		return $this;
571
	}
572
573
574
	public function rotate(Meta $meta, $deg = 90)
575
	{
576
		$original = $this->createFilename($meta);
577
		$backupName = $this->cacheDirectory . '/__rotate-backup';
578
579
		// Create rotated image
580
		copy($original, $backupName);
581
582
		exec(sprintf("convert -rotate $deg -strip %s %s", $original, $backupName));
583
584
		// Add new rotated image to storage
585
		$this->add(new ImageFile($backupName), $meta);
586
587
		unlink($backupName);
588
589
		return $this;
590
	}
591
592
593
	/**
594
	 * Adds an image from the given file to the storage.
595
	 *
596
	 * @param ImageFile $file The image file to add
597
	 * @param Meta      $meta
598
	 * @return boolean TRUE on success or FALSE on failure
599
	 */
600
	public function add(ImageFile $file, Meta $meta)
601
	{
602
		$this->readMeta($file->getName(), $meta);
603
604
		return copy($file->getName(), $this->createFilename($meta));
605
	}
606
607
608
}
609