Completed
Push — 3.2 ( c3544f...9dabf1 )
by Daniel
11:59
created

Image   D

Complexity

Total Complexity 154

Size/Duplication

Total Lines 993
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 11
Metric Value
wmc 154
lcom 2
cbo 11
dl 0
loc 993
rs 4

59 Methods

Rating   Name   Duplication   Size   Complexity  
A flush() 0 3 1
A set_backend() 0 3 1
A get_backend() 0 3 1
A defineMethods() 0 10 3
A getCMSFields() 0 14 1
B getTag() 0 14 5
A forTemplate() 0 3 1
D loadUploadedImage() 0 44 12
B Fit() 0 19 8
A generateFit() 0 3 1
B FitMax() 0 8 6
A Fill() 0 5 3
A generateFill() 0 3 1
B FillMax() 0 20 9
A Pad() 0 5 3
A generatePad() 0 3 1
A ScaleWidth() 0 5 3
A generateScaleWidth() 0 3 1
A ScaleMaxWidth() 0 8 4
A ScaleHeight() 0 5 3
A generateScaleHeight() 0 3 1
A ScaleMaxHeight() 0 8 4
A CropWidth() 0 8 4
A CropHeight() 0 8 4
A SetRatioSize() 0 4 1
A generateSetRatioSize() 0 4 1
A SetWidth() 0 4 1
A generateSetWidth() 0 4 1
A SetHeight() 0 4 1
A generateSetHeight() 0 4 1
A SetSize() 0 4 1
A generateSetSize() 0 4 1
A CMSThumbnail() 0 3 1
A generateCMSThumbnail() 0 3 1
A generateAssetLibraryPreview() 0 3 1
A generateAssetLibraryThumbnail() 0 3 1
A generateStripThumbnail() 0 3 1
A PaddedImage() 0 4 1
A generatePaddedImage() 0 4 1
A isSize() 0 3 2
A isWidth() 0 3 2
A isHeight() 0 3 2
A getFormattedImage() 0 18 4
A cacheFilename() 0 15 3
B generateFormattedImage() 0 28 4
A ResizedImage() 0 5 3
A generateResizedImage() 0 8 2
A CroppedImage() 0 4 1
A generateCroppedImage() 0 4 1
A getFilenamePatterns() 0 18 3
C getGeneratedImages() 0 45 11
B regenerateFormattedImages() 0 24 5
A deleteFormattedImages() 0 12 3
B getDimensions() 0 12 5
A getWidth() 0 3 1
A getHeight() 0 3 1
A getOrientation() 0 11 3
A onAfterUpload() 0 4 1
A onBeforeDelete() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like Image often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Image, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Represents an Image
5
 *
6
 * @package framework
7
 * @subpackage filesystem
8
 */
9
class Image extends File implements Flushable {
10
11
	const ORIENTATION_SQUARE = 0;
12
	const ORIENTATION_PORTRAIT = 1;
13
	const ORIENTATION_LANDSCAPE = 2;
14
15
	private static $backend = "GDBackend";
16
17
	private static $casting = array(
18
		'Tag' => 'HTMLText',
19
	);
20
21
	/**
22
	 * @config
23
	 * @var int The width of an image thumbnail in a strip.
24
	 */
25
	private static $strip_thumbnail_width = 50;
26
27
	/**
28
	 * @config
29
	 * @var int The height of an image thumbnail in a strip.
30
	 */
31
	private static $strip_thumbnail_height = 50;
32
33
	/**
34
	 * @config
35
	 * @var int The width of an image thumbnail in the CMS.
36
	 */
37
	private static $cms_thumbnail_width = 100;
38
39
	/**
40
	 * @config
41
	 * @var int The height of an image thumbnail in the CMS.
42
	 */
43
	private static $cms_thumbnail_height = 100;
44
45
	/**
46
	 * @config
47
	 * @var int The width of an image thumbnail in the Asset section.
48
	 */
49
	private static $asset_thumbnail_width = 100;
50
51
	/**
52
	 * @config
53
	 * @var int The height of an image thumbnail in the Asset section.
54
	 */
55
	private static $asset_thumbnail_height = 100;
56
57
	/**
58
	 * @config
59
	 * @var int The width of an image preview in the Asset section.
60
	 */
61
	private static $asset_preview_width = 400;
62
63
	/**
64
	 * @config
65
	 * @var int The height of an image preview in the Asset section.
66
	 */
67
	private static $asset_preview_height = 200;
68
69
	/**
70
	 * @config
71
	 * @var bool Force all images to resample in all cases
72
	 */
73
	private static $force_resample = false;
74
75
	/**
76
	 * @config
77
	 * @var bool Regenerates images if set to true. This is set by {@link flush()}
78
	 */
79
	private static $flush = false;
80
81
	/**
82
	 * Triggered early in the request when someone requests a flush.
83
	 */
84
	public static function flush() {
85
		self::$flush = true;
86
	}
87
88
	public static function set_backend($backend) {
89
		self::config()->backend = $backend;
0 ignored issues
show
Documentation introduced by
The property backend does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
90
	}
91
92
	public static function get_backend() {
93
		return self::config()->backend;
94
	}
95
96
	/**
97
	 * Set up template methods to access the transformations generated by 'generate' methods.
98
	 */
99
	public function defineMethods() {
100
		$methodNames = $this->allMethodNames();
101
		foreach($methodNames as $methodName) {
102
			if(substr($methodName,0,8) == 'generate') {
103
				$this->addWrapperMethod(substr($methodName,8), 'getFormattedImage');
104
			}
105
		}
106
107
		parent::defineMethods();
108
	}
109
110
	public function getCMSFields() {
111
		$fields = parent::getCMSFields();
112
113
		$urlLink = "<div class='field readonly'>";
114
		$urlLink .= "<label class='left'>"._t('AssetTableField.URL','URL')."</label>";
115
		$urlLink .= "<span class='readonly'><a href='{$this->Link()}'>{$this->RelativeLink()}</a></span>";
116
		$urlLink .= "</div>";
117
118
		//attach the addition file information for an image to the existing FieldGroup create in the parent class
119
		$fileAttributes = $fields->fieldByName('Root.Main.FilePreview')->fieldByName('FilePreviewData');
120
		$fileAttributes->push(new ReadonlyField("Dimensions", _t('AssetTableField.DIM','Dimensions') . ':'));
121
122
		return $fields;
123
	}
124
125
	/**
126
	 * Return an XHTML img tag for this Image,
127
	 * or NULL if the image file doesn't exist on the filesystem.
128
	 *
129
	 * @return string
130
	 */
131
	public function getTag() {
132
		if($this->exists()) {
133
			$url = $this->getURL();
134
			$title = ($this->Title) ? $this->Title : $this->Filename;
135
			if($this->Title) {
136
				$title = Convert::raw2att($this->Title);
137
			} else {
138
				if(preg_match("/([^\/]*)\.[a-zA-Z0-9]{1,6}$/", $title, $matches)) {
139
					$title = Convert::raw2att($matches[1]);
140
				}
141
			}
142
			return "<img src=\"$url\" alt=\"$title\" />";
143
		}
144
	}
145
146
	/**
147
	 * Return an XHTML img tag for this Image.
148
	 *
149
	 * @return string
150
	 */
151
	public function forTemplate() {
152
		return $this->getTag();
153
	}
154
155
	/**
156
	 * File names are filtered through {@link FileNameFilter}, see class documentation
157
	 * on how to influence this behaviour.
158
	 *
159
	 * @deprecated 4.0
160
	 */
161
	public function loadUploadedImage($tmpFile) {
162
		Deprecation::notice('4.0', 'Use the Upload::loadIntoFile()');
163
164
		if(!is_array($tmpFile)) {
165
			user_error("Image::loadUploadedImage() Not passed an array.  Most likely, the form hasn't got the right"
166
				. "enctype", E_USER_ERROR);
167
		}
168
169
		if(!$tmpFile['size']) {
170
			return;
171
		}
172
173
		$class = $this->class;
174
175
		// Create a folder
176
		if(!file_exists(ASSETS_PATH)) {
177
			mkdir(ASSETS_PATH, Config::inst()->get('Filesystem', 'folder_create_mask'));
178
		}
179
180
		if(!file_exists(ASSETS_PATH . "/$class")) {
181
			mkdir(ASSETS_PATH . "/$class", Config::inst()->get('Filesystem', 'folder_create_mask'));
182
		}
183
184
		// Generate default filename
185
		$nameFilter = FileNameFilter::create();
186
		$file = $nameFilter->filter($tmpFile['name']);
187
		if(!$file) $file = "file.jpg";
188
189
		$file = ASSETS_PATH . "/$class/$file";
190
191
		while(file_exists(BASE_PATH . "/$file")) {
192
			$i = $i ? ($i+1) : 2;
0 ignored issues
show
Bug introduced by
The variable $i does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
193
			$oldFile = $file;
194
			$file = preg_replace('/[0-9]*(\.[^.]+$)/', $i . '\\1', $file);
195
			if($oldFile == $file && $i > 2) user_error("Couldn't fix $file with $i", E_USER_ERROR);
196
		}
197
198
		if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], BASE_PATH . "/$file")) {
199
			// Remove the old images
200
201
			$this->deleteFormattedImages();
202
			return true;
203
		}
204
	}
205
206
	/**
207
	 * Scale image proportionally to fit within the specified bounds
208
	 *
209
	 * @param integer $width The width to size within
210
	 * @param integer $height The height to size within
211
	 * @return Image|null
212
	 */
213
	public function Fit($width, $height) {
214
		// Prevent divide by zero on missing/blank file
215
		if(!$this->getWidth() || !$this->getHeight()) return null;
216
217
		// Check if image is already sized to the correct dimension
218
		$widthRatio = $width / $this->getWidth();
219
		$heightRatio = $height / $this->getHeight();
220
221
		if( $widthRatio < $heightRatio ) {
222
			// Target is higher aspect ratio than image, so check width
223
			if($this->isWidth($width) && !Config::inst()->get('Image', 'force_resample')) return $this;
224
		} else {
225
			// Target is wider or same aspect ratio as image, so check height
226
			if($this->isHeight($height) && !Config::inst()->get('Image', 'force_resample')) return $this;
227
		}
228
229
		// Item must be regenerated
230
		return  $this->getFormattedImage('Fit', $width, $height);
231
	}
232
233
	/**
234
	 * Scale image proportionally to fit within the specified bounds
235
	 *
236
	 * @param Image_Backend $backend
237
	 * @param integer $width The width to size within
238
	 * @param integer $height The height to size within
239
	 * @return Image_Backend
240
	 */
241
	public function generateFit(Image_Backend $backend, $width, $height) {
242
		return $backend->resizeRatio($width, $height);
243
	}
244
245
	/**
246
	 * Proportionally scale down this image if it is wider or taller than the specified dimensions.
247
	 * Similar to Fit but without up-sampling. Use in templates with $FitMax.
248
	 *
249
	 * @uses Image::Fit()
250
	 * @param integer $width The maximum width of the output image
251
	 * @param integer $height The maximum height of the output image
252
	 * @return Image
253
	 */
254
	public function FitMax($width, $height) {
255
		// Temporary $force_resample support for 3.x, to be removed in 4.0
256
		if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width && $this->getHeight() <= $height) return $this->Fit($this->getWidth(),$this->getHeight());
257
258
		return $this->getWidth() > $width || $this->getHeight() > $height
259
			? $this->Fit($width,$height)
260
			: $this;
261
	}
262
263
	/**
264
	 * Resize and crop image to fill specified dimensions.
265
	 * Use in templates with $Fill
266
	 *
267
	 * @param integer $width Width to crop to
268
	 * @param integer $height Height to crop to
269
	 * @return Image|null
270
	 */
271
	public function Fill($width, $height) {
272
		return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample')
273
			? $this
274
			: $this->getFormattedImage('Fill', $width, $height);
275
	}
276
277
	/**
278
	 * Resize and crop image to fill specified dimensions.
279
	 * Use in templates with $Fill
280
	 *
281
	 * @param Image_Backend $backend
282
	 * @param integer $width Width to crop to
283
	 * @param integer $height Height to crop to
284
	 * @return Image_Backend
285
	 */
286
	public function generateFill(Image_Backend $backend, $width, $height) {
287
		return $backend->croppedResize($width, $height);
288
	}
289
290
	/**
291
	 * Crop this image to the aspect ratio defined by the specified width and height,
292
	 * then scale down the image to those dimensions if it exceeds them.
293
	 * Similar to Fill but without up-sampling. Use in templates with $FillMax.
294
	 *
295
	 * @uses Image::Fill()
296
	 * @param integer $width The relative (used to determine aspect ratio) and maximum width of the output image
297
	 * @param integer $height The relative (used to determine aspect ratio) and maximum height of the output image
298
	 * @return Image
299
	 */
300
	public function FillMax($width, $height) {
301
		// Prevent divide by zero on missing/blank file
302
		if(!$this->getWidth() || !$this->getHeight()) return null;
303
304
		// Temporary $force_resample support for 3.x, to be removed in 4.0
305
		if (Config::inst()->get('Image', 'force_resample') && $this->isSize($width, $height)) return $this->Fill($width, $height);
306
307
		// Is the image already the correct size?
308
		if ($this->isSize($width, $height)) return $this;
309
310
		// If not, make sure the image isn't upsampled
311
		$imageRatio = $this->getWidth() / $this->getHeight();
312
		$cropRatio = $width / $height;
313
		// If cropping on the x axis compare heights
314
		if ($cropRatio < $imageRatio && $this->getHeight() < $height) return $this->Fill($this->getHeight()*$cropRatio, $this->getHeight());
315
		// Otherwise we're cropping on the y axis (or not cropping at all) so compare widths
316
		if ($this->getWidth() < $width) return $this->Fill($this->getWidth(), $this->getWidth()/$cropRatio);
317
318
		return $this->Fill($width, $height);
319
	}
320
321
	/**
322
	 * Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad.
323
	 *
324
	 * @param integer $width The width to size to
325
	 * @param integer $height The height to size to
326
	 * @return Image|null
327
	 */
328
	public function Pad($width, $height, $backgroundColor='FFFFFF') {
329
		return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample')
330
			? $this
331
			: $this->getFormattedImage('Pad', $width, $height, $backgroundColor);
332
	}
333
334
	/**
335
	 * Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad.
336
	 *
337
	 * @param Image_Backend $backend
338
	 * @param integer $width The width to size to
339
	 * @param integer $height The height to size to
340
	 * @return Image_Backend
341
	 */
342
	public function generatePad(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') {
343
		return $backend->paddedResize($width, $height, $backgroundColor);
344
	}
345
346
	/**
347
	 * Scale image proportionally by width. Use in templates with $ScaleWidth.
348
	 *
349
	 * @param integer $width The width to set
350
	 * @return Image|null
351
	 */
352
	public function ScaleWidth($width) {
353
		return $this->isWidth($width) && !Config::inst()->get('Image', 'force_resample')
354
			? $this
355
			: $this->getFormattedImage('ScaleWidth', $width);
356
	}
357
358
	/**
359
	 * Scale image proportionally by width. Use in templates with $ScaleWidth.
360
	 *
361
	 * @param Image_Backend $backend
362
	 * @param int $width The width to set
363
	 * @return Image_Backend
364
	 */
365
	public function generateScaleWidth(Image_Backend $backend, $width) {
366
		return $backend->resizeByWidth($width);
367
	}
368
369
	/**
370
	 * Proportionally scale down this image if it is wider than the specified width.
371
	 * Similar to ScaleWidth but without up-sampling. Use in templates with $ScaleMaxWidth.
372
	 *
373
	 * @uses Image::ScaleWidth()
374
	 * @param integer $width The maximum width of the output image
375
	 * @return Image
376
	 */
377
	public function ScaleMaxWidth($width) {
378
		// Temporary $force_resample support for 3.x, to be removed in 4.0
379
		if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width) return $this->ScaleWidth($this->getWidth());
380
381
		return $this->getWidth() > $width
382
			? $this->ScaleWidth($width)
383
			: $this;
384
	}
385
386
	/**
387
	 * Scale image proportionally by height. Use in templates with $ScaleHeight.
388
	 *
389
	 * @param integer $height The height to set
390
	 * @return Image|null
391
	 */
392
	public function ScaleHeight($height) {
393
		return $this->isHeight($height) && !Config::inst()->get('Image', 'force_resample')
394
			? $this
395
			: $this->getFormattedImage('ScaleHeight', $height);
396
	}
397
398
	/**
399
	 * Scale image proportionally by height. Use in templates with $ScaleHeight.
400
	 *
401
	 * @param Image_Backend $backend
402
	 * @param integer $height The height to set
403
	 * @return Image_Backend
404
	 */
405
	public function generateScaleHeight(Image_Backend $backend, $height){
406
		return $backend->resizeByHeight($height);
407
	}
408
409
	/**
410
	 * Proportionally scale down this image if it is taller than the specified height.
411
	 * Similar to ScaleHeight but without up-sampling. Use in templates with $ScaleMaxHeight.
412
	 *
413
	 * @uses Image::ScaleHeight()
414
	 * @param integer $height The maximum height of the output image
415
	 * @return Image
416
	 */
417
	public function ScaleMaxHeight($height) {
418
		// Temporary $force_resample support for 3.x, to be removed in 4.0
419
		if (Config::inst()->get('Image', 'force_resample') && $this->getHeight() <= $height) return $this->ScaleHeight($this->getHeight());
420
421
		return $this->getHeight() > $height
422
			? $this->ScaleHeight($height)
423
			: $this;
424
	}
425
426
	/**
427
	 * Crop image on X axis if it exceeds specified width. Retain height.
428
	 * Use in templates with $CropWidth. Example: $Image.ScaleHeight(100).$CropWidth(100)
429
	 *
430
	 * @uses Image::Fill()
431
	 * @param integer $width The maximum width of the output image
432
	 * @return Image
433
	 */
434
	public function CropWidth($width) {
435
		// Temporary $force_resample support for 3.x, to be removed in 4.0
436
		if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width) return $this->Fill($this->getWidth(), $this->getHeight());
437
438
		return $this->getWidth() > $width
439
			? $this->Fill($width, $this->getHeight())
440
			: $this;
441
	}
442
443
	/**
444
	 * Crop image on Y axis if it exceeds specified height. Retain width.
445
	 * Use in templates with $CropHeight. Example: $Image.ScaleWidth(100).CropHeight(100)
446
	 *
447
	 * @uses Image::Fill()
448
	 * @param integer $height The maximum height of the output image
449
	 * @return Image
450
	 */
451
	public function CropHeight($height) {
452
		// Temporary $force_resample support for 3.x, to be removed in 4.0
453
		if (Config::inst()->get('Image', 'force_resample') && $this->getHeight() <= $height) return $this->Fill($this->getWidth(), $this->getHeight());
454
455
		return $this->getHeight() > $height
456
			? $this->Fill($this->getWidth(), $height)
457
			: $this;
458
	}
459
460
	/**
461
	 * Resize the image by preserving aspect ratio, keeping the image inside the
462
	 * $width and $height
463
	 *
464
	 * @param integer $width The width to size within
465
	 * @param integer $height The height to size within
466
	 * @return Image
467
	 * @deprecated 4.0 Use Fit instead
468
	 */
469
	public function SetRatioSize($width, $height) {
470
		Deprecation::notice('4.0', 'Use Fit instead');
471
		return $this->Fit($width, $height);
472
	}
473
474
	/**
475
	 * Resize the image by preserving aspect ratio, keeping the image inside the
476
	 * $width and $height
477
	 *
478
	 * @param Image_Backend $backend
479
	 * @param integer $width The width to size within
480
	 * @param integer $height The height to size within
481
	 * @return Image_Backend
482
	 * @deprecated 4.0 Use generateFit instead
483
	 */
484
	public function generateSetRatioSize(Image_Backend $backend, $width, $height) {
485
		Deprecation::notice('4.0', 'Use generateFit instead');
486
		return $backend->resizeRatio($width, $height);
487
	}
488
489
	/**
490
	 * Resize this Image by width, keeping aspect ratio. Use in templates with $SetWidth.
491
	 *
492
	 * @param integer $width The width to set
493
	 * @return Image
494
	 * @deprecated 4.0 Use ScaleWidth instead
495
	 */
496
	public function SetWidth($width) {
497
		Deprecation::notice('4.0', 'Use ScaleWidth instead');
498
		return $this->ScaleWidth($width);
499
	}
500
501
	/**
502
	 * Resize this Image by width, keeping aspect ratio. Use in templates with $SetWidth.
503
	 *
504
	 * @param Image_Backend $backend
505
	 * @param int $width The width to set
506
	 * @return Image_Backend
507
	 * @deprecated 4.0 Use generateScaleWidth instead
508
	 */
509
	public function generateSetWidth(Image_Backend $backend, $width) {
510
		Deprecation::notice('4.0', 'Use generateScaleWidth instead');
511
		return $backend->resizeByWidth($width);
512
	}
513
514
	/**
515
	 * Resize this Image by height, keeping aspect ratio. Use in templates with $SetHeight.
516
	 *
517
	 * @param integer $height The height to set
518
	 * @return Image
519
	 * @deprecated 4.0 Use ScaleHeight instead
520
	 */
521
	public function SetHeight($height) {
522
		Deprecation::notice('4.0', 'Use ScaleHeight instead');
523
		return $this->ScaleHeight($height);
524
	}
525
526
	/**
527
	 * Resize this Image by height, keeping aspect ratio. Use in templates with $SetHeight.
528
	 *
529
	 * @param Image_Backend $backend
530
	 * @param integer $height The height to set
531
	 * @return Image_Backend
532
	 * @deprecated 4.0 Use generateScaleHeight instead
533
	 */
534
	public function generateSetHeight(Image_Backend $backend, $height){
535
		Deprecation::notice('4.0', 'Use generateScaleHeight instead');
536
		return $backend->resizeByHeight($height);
537
	}
538
539
	/**
540
	 * Resize this Image by both width and height, using padded resize. Use in templates with $SetSize.
541
	 * @see Image::PaddedImage()
542
	 *
543
	 * @param integer $width The width to size to
544
	 * @param integer $height The height to size to
545
	 * @return Image
546
	 * @deprecated 4.0 Use Pad instead
547
	 */
548
	public function SetSize($width, $height) {
549
		Deprecation::notice('4.0', 'Use Pad instead');
550
		return $this->Pad($width, $height);
551
	}
552
553
	/**
554
	 * Resize this Image by both width and height, using padded resize. Use in templates with $SetSize.
555
	 *
556
	 * @param Image_Backend $backend
557
	 * @param integer $width The width to size to
558
	 * @param integer $height The height to size to
559
	 * @return Image_Backend
560
	 * @deprecated 4.0 Use generatePad instead
561
	 */
562
	public function generateSetSize(Image_Backend $backend, $width, $height) {
563
		Deprecation::notice('4.0', 'Use generatePad instead');
564
		return $backend->paddedResize($width, $height);
565
	}
566
567
	/**
568
	 * Resize this image for the CMS. Use in templates with $CMSThumbnail
569
	 *
570
	 * @return Image_Cached|null
571
	 */
572
	public function CMSThumbnail() {
573
		return $this->getFormattedImage('CMSThumbnail');
574
	}
575
576
	/**
577
	 * Resize this image for the CMS. Use in templates with $CMSThumbnail.
578
	 * @return Image_Backend
579
	 */
580
	public function generateCMSThumbnail(Image_Backend $backend) {
581
		return $backend->paddedResize($this->stat('cms_thumbnail_width'),$this->stat('cms_thumbnail_height'));
582
	}
583
584
	/**
585
	 * Resize this image for preview in the Asset section. Use in templates with $AssetLibraryPreview.
586
	 * @return Image_Backend
587
	 */
588
	public function generateAssetLibraryPreview(Image_Backend $backend) {
589
		return $backend->paddedResize($this->stat('asset_preview_width'),$this->stat('asset_preview_height'));
590
	}
591
592
	/**
593
	 * Resize this image for thumbnail in the Asset section. Use in templates with $AssetLibraryThumbnail.
594
	 * @return Image_Backend
595
	 */
596
	public function generateAssetLibraryThumbnail(Image_Backend $backend) {
597
		return $backend->paddedResize($this->stat('asset_thumbnail_width'),$this->stat('asset_thumbnail_height'));
598
	}
599
600
	/**
601
	 * Resize this image for use as a thumbnail in a strip. Use in templates with $StripThumbnail.
602
	 * @return Image_Backend
603
	 */
604
	public function generateStripThumbnail(Image_Backend $backend) {
605
		return $backend->croppedResize($this->stat('strip_thumbnail_width'),$this->stat('strip_thumbnail_height'));
606
	}
607
608
	/**
609
	 * Resize this Image by both width and height, using padded resize. Use in templates with $PaddedImage.
610
	 * @see Image::SetSize()
611
	 *
612
	 * @param integer $width The width to size to
613
	 * @param integer $height The height to size to
614
	 * @return Image
615
	 * @deprecated 4.0 Use Pad instead
616
	 */
617
	public function PaddedImage($width, $height, $backgroundColor='FFFFFF') {
618
		Deprecation::notice('4.0', 'Use Pad instead');
619
		return $this->Pad($width, $height, $backgroundColor);
620
	}
621
622
	/**
623
	 * Resize this Image by both width and height, using padded resize. Use in templates with $PaddedImage.
624
	 *
625
	 * @param Image_Backend $backend
626
	 * @param integer $width The width to size to
627
	 * @param integer $height The height to size to
628
	 * @return Image_Backend
629
	 * @deprecated 4.0 Use generatePad instead
630
	 */
631
	public function generatePaddedImage(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') {
632
		Deprecation::notice('4.0', 'Use generatePad instead');
633
		return $backend->paddedResize($width, $height, $backgroundColor);
634
	}
635
636
	/**
637
	 * Determine if this image is of the specified size
638
	 *
639
	 * @param integer $width Width to check
640
	 * @param integer $height Height to check
641
	 * @return boolean
642
	 */
643
	public function isSize($width, $height) {
644
		return $this->isWidth($width) && $this->isHeight($height);
645
	}
646
647
	/**
648
	 * Determine if this image is of the specified width
649
	 *
650
	 * @param integer $width Width to check
651
	 * @return boolean
652
	 */
653
	public function isWidth($width) {
654
		return !empty($width) && $this->getWidth() == $width;
655
	}
656
657
	/**
658
	 * Determine if this image is of the specified width
659
	 *
660
	 * @param integer $height Height to check
661
	 * @return boolean
662
	 */
663
	public function isHeight($height) {
664
		return !empty($height) && $this->getHeight() == $height;
665
	}
666
667
	/**
668
	 * Return an image object representing the image in the given format.
669
	 * This image will be generated using generateFormattedImage().
670
	 * The generated image is cached, to flush the cache append ?flush=1 to your URL.
671
	 *
672
	 * Just pass the correct number of parameters expected by the working function
673
	 *
674
	 * @param string $format The name of the format.
675
	 * @return Image_Cached|null
676
	 */
677
	public function getFormattedImage($format) {
0 ignored issues
show
Unused Code introduced by
The parameter $format is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
678
		$args = func_get_args();
679
680
		if($this->exists()) {
681
			$cacheFile = call_user_func_array(array($this, "cacheFilename"), $args);
682
683
			if(!file_exists(Director::baseFolder()."/".$cacheFile) || self::$flush) {
684
				call_user_func_array(array($this, "generateFormattedImage"), $args);
685
			}
686
687
			$cached = new Image_Cached($cacheFile);
688
			// Pass through the title so the templates can use it
689
			$cached->Title = $this->Title;
690
			// Pass through the parent, to store cached images in correct folder.
691
			$cached->ParentID = $this->ParentID;
692
			return $cached;
693
		}
694
	}
695
696
	/**
697
	 * Return the filename for the cached image, given its format name and arguments.
698
	 * @param string $format The format name.
699
	 * @return string
700
	 * @throws InvalidArgumentException
701
	 */
702
	public function cacheFilename($format) {
703
		$args = func_get_args();
704
		array_shift($args);
705
		$folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . "/";
706
707
		$format = $format . Convert::base64url_encode($args);
708
		$filename = $format . "-" . $this->Name;
709
		$patterns = $this->getFilenamePatterns($this->Name);
710
		if (!preg_match($patterns['FullPattern'], $filename)) {
711
			throw new InvalidArgumentException('Filename ' . $filename
712
				. ' that should be used to cache a resized image is invalid');
713
		}
714
715
		return $folder . "_resampled/" . $filename;
716
	}
717
718
	/**
719
	 * Generate an image on the specified format. It will save the image
720
	 * at the location specified by cacheFilename(). The image will be generated
721
	 * using the specific 'generate' method for the specified format.
722
	 *
723
	 * @param string $format Name of the format to generate.
724
	 */
725
	public function generateFormattedImage($format) {
726
		$args = func_get_args();
727
728
		$cacheFile = call_user_func_array(array($this, "cacheFilename"), $args);
729
730
		$backend = Injector::inst()->createWithArgs(self::config()->backend, array(
731
			Director::baseFolder()."/" . $this->Filename,
732
			$args
733
		));
734
735
		if($backend->hasImageResource()) {
736
737
			$generateFunc = "generate$format";
738
			if($this->hasMethod($generateFunc)){
739
740
				array_shift($args);
741
				array_unshift($args, $backend);
742
743
				$backend = call_user_func_array(array($this, $generateFunc), $args);
744
				if($backend){
745
					$backend->writeTo(Director::baseFolder()."/" . $cacheFile);
746
				}
747
748
			} else {
749
				user_error("Image::generateFormattedImage - Image $format public function not found.",E_USER_WARNING);
750
			}
751
		}
752
	}
753
754
	/**
755
	 * Generate a resized copy of this image with the given width & height.
756
	 * This can be used in templates with $ResizedImage but should be avoided,
757
	 * as it's the only image manipulation function which can skew an image.
758
	 *
759
	 * @param integer $width Width to resize to
760
	 * @param integer $height Height to resize to
761
	 * @return Image
762
	 */
763
	public function ResizedImage($width, $height) {
764
		return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample')
765
			? $this
766
			: $this->getFormattedImage('ResizedImage', $width, $height);
767
	}
768
769
	/**
770
	 * Generate a resized copy of this image with the given width & height.
771
	 * Use in templates with $ResizedImage.
772
	 *
773
	 * @param Image_Backend $backend
774
	 * @param integer $width Width to resize to
775
	 * @param integer $height Height to resize to
776
	 * @return Image_Backend|null
777
	 */
778
	public function generateResizedImage(Image_Backend $backend, $width, $height) {
779
		if(!$backend){
780
			user_error("Image::generateFormattedImage - generateResizedImage is being called by legacy code"
781
				. " or Image::\$backend is not set.",E_USER_WARNING);
782
		}else{
783
			return $backend->resize($width, $height);
784
		}
785
	}
786
787
	/**
788
	 * Generate a resized copy of this image with the given width & height, cropping to maintain aspect ratio.
789
	 * Use in templates with $CroppedImage
790
	 *
791
	 * @param integer $width Width to crop to
792
	 * @param integer $height Height to crop to
793
	 * @return Image
794
	 * @deprecated 4.0 Use Fill instead
795
	 */
796
	public function CroppedImage($width, $height) {
797
		Deprecation::notice('4.0', 'Use Fill instead');
798
		return $this->Fill($width, $height);
799
	}
800
801
	/**
802
	 * Generate a resized copy of this image with the given width & height, cropping to maintain aspect ratio.
803
	 * Use in templates with $CroppedImage
804
	 *
805
	 * @param Image_Backend $backend
806
	 * @param integer $width Width to crop to
807
	 * @param integer $height Height to crop to
808
	 * @return Image_Backend
809
	 * @deprecated 4.0 Use generateFill instead
810
	 */
811
	public function generateCroppedImage(Image_Backend $backend, $width, $height) {
812
		Deprecation::notice('4.0', 'Use generateFill instead');
813
		return $backend->croppedResize($width, $height);
814
	}
815
816
	/**
817
	 * Generate patterns that will help to match filenames of cached images
818
	 * @param string $filename Filename of source image
819
	 * @return array
820
	 */
821
	private function getFilenamePatterns($filename) {
822
		$methodNames = $this->allMethodNames(true);
823
		$generateFuncs = array();
824
		foreach($methodNames as $methodName) {
825
			if(substr($methodName, 0, 8) == 'generate') {
826
				$format = substr($methodName, 8);
827
				$generateFuncs[] = preg_quote($format);
828
			}
829
		}
830
		// All generate functions may appear any number of times in the image cache name.
831
		$generateFuncs = implode('|', $generateFuncs);
832
		$base64url_match = "[a-zA-Z0-9_~]*={0,2}";
833
		return array(
834
				'FullPattern' => "/^((?P<Generator>{$generateFuncs})(?P<Args>" . $base64url_match . ")\-)+"
835
									. preg_quote($filename) . "$/i",
836
				'GeneratorPattern' => "/(?P<Generator>{$generateFuncs})(?P<Args>" . $base64url_match . ")\-/i"
837
		);
838
	}
839
840
	/**
841
	 * Generate a list of images that were generated from this image
842
	 */
843
	private function getGeneratedImages() {
844
		$generatedImages = array();
845
		$cachedFiles = array();
846
847
		$folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . '/';
848
		$cacheDir = Director::getAbsFile($folder . '_resampled/');
849
850
		if(is_dir($cacheDir)) {
851
			if($handle = opendir($cacheDir)) {
852
				while(($file = readdir($handle)) !== false) {
853
					// ignore all entries starting with a dot
854
					if(substr($file, 0, 1) != '.' && is_file($cacheDir . $file)) {
855
						$cachedFiles[] = $file;
856
					}
857
				}
858
				closedir($handle);
859
			}
860
		}
861
862
		$pattern = $this->getFilenamePatterns($this->Name);
863
864
		foreach($cachedFiles as $cfile) {
865
			if(preg_match($pattern['FullPattern'], $cfile, $matches)) {
866
				if(Director::fileExists($cacheDir . $cfile)) {
867
					$subFilename = substr($cfile, 0, -1 * strlen($this->Name));
868
					preg_match_all($pattern['GeneratorPattern'], $subFilename, $subMatches, PREG_SET_ORDER);
869
870
					$generatorArray = array();
871
					foreach ($subMatches as $singleMatch) {
872
						$generatorArray[] = array('Generator' => $singleMatch['Generator'],
873
						'Args' => Convert::base64url_decode($singleMatch['Args']));
874
					}
875
876
						// Using array_reverse is important, as a cached image will
877
						// have the generators settings in the filename in reversed
878
						// order: the last generator given in the filename is the
879
						// first that was used. Later resizements are prepended
880
					$generatedImages[] = array ( 'FileName' => $cacheDir . $cfile,
881
							'Generators' => array_reverse($generatorArray) );
882
				}
883
			}
884
		}
885
886
		return $generatedImages;
887
	}
888
889
	/**
890
	 * Regenerate all of the formatted cached images for this image.
891
	 *
892
	 * @return int The number of formatted images regenerated
893
	 */
894
	public function regenerateFormattedImages() {
895
		if(!$this->Filename) return 0;
896
897
		// Without this, not a single file would be written
898
		// caused by a check in getFormattedImage()
899
		$this->flush();
900
901
		$numGenerated = 0;
902
		$generatedImages = $this->getGeneratedImages();
903
		$doneList = array();
904
		foreach($generatedImages as $singleImage) {
905
			$cachedImage = $this;
906
			if (in_array($singleImage['FileName'], $doneList) ) continue;
907
908
			foreach($singleImage['Generators'] as $singleGenerator) {
909
				$args = array_merge(array($singleGenerator['Generator']), $singleGenerator['Args']);
910
				$cachedImage = call_user_func_array(array($cachedImage, "getFormattedImage"), $args);
911
			}
912
			$doneList[] = $singleImage['FileName'];
913
			$numGenerated++;
914
		}
915
916
		return $numGenerated;
917
	}
918
919
	/**
920
	 * Remove all of the formatted cached images for this image.
921
	 *
922
	 * @return int The number of formatted images deleted
923
	 */
924
	public function deleteFormattedImages() {
925
		if(!$this->Filename) return 0;
926
927
		$numDeleted = 0;
928
		$generatedImages = $this->getGeneratedImages();
929
		foreach($generatedImages as $singleImage) {
930
			unlink($singleImage['FileName']);
931
			$numDeleted++;
932
		}
933
934
		return $numDeleted;
935
	}
936
937
	/**
938
	 * Get the dimensions of this Image.
939
	 * @param string $dim If this is equal to "string", return the dimensions in string form,
940
	 * if it is 0 return the height, if it is 1 return the width.
941
	 * @return string|int|null
942
	 */
943
	public function getDimensions($dim = "string") {
944
		if($this->getField('Filename')) {
945
946
			$imagefile = $this->getFullPath();
947
			if($this->exists()) {
948
				$size = getimagesize($imagefile);
949
				return ($dim === "string") ? "$size[0]x$size[1]" : $size[$dim];
950
			} else {
951
				return ($dim === "string") ? "file '$imagefile' not found" : null;
952
			}
953
		}
954
	}
955
956
	/**
957
	 * Get the width of this image.
958
	 * @return int
959
	 */
960
	public function getWidth() {
961
		return $this->getDimensions(0);
962
	}
963
964
	/**
965
	 * Get the height of this image.
966
	 * @return int
967
	 */
968
	public function getHeight() {
969
		return $this->getDimensions(1);
970
	}
971
972
	/**
973
	 * Get the orientation of this image.
974
	 * @return ORIENTATION_SQUARE | ORIENTATION_PORTRAIT | ORIENTATION_LANDSCAPE
975
	 */
976
	public function getOrientation() {
977
		$width = $this->getWidth();
978
		$height = $this->getHeight();
979
		if($width > $height) {
980
			return self::ORIENTATION_LANDSCAPE;
981
		} elseif($height > $width) {
982
			return self::ORIENTATION_PORTRAIT;
983
		} else {
984
			return self::ORIENTATION_SQUARE;
985
		}
986
	}
987
988
	public function onAfterUpload() {
989
		$this->deleteFormattedImages();
990
		parent::onAfterUpload();
991
	}
992
993
	protected function onBeforeDelete() {
994
		$backend = Injector::inst()->create(self::get_backend());
995
		$backend->onBeforeDelete($this);
996
997
		$this->deleteFormattedImages();
998
999
		parent::onBeforeDelete();
1000
	}
1001
}
1002
1003
/**
1004
 * A resized / processed {@link Image} object.
1005
 * When Image object are processed or resized, a suitable Image_Cached object is returned, pointing to the
1006
 * cached copy of the processed image.
1007
 *
1008
 * @package framework
1009
 * @subpackage filesystem
1010
 */
1011
class Image_Cached extends Image {
1012
1013
	/**
1014
	 * Create a new cached image.
1015
	 * @param string $filename The filename of the image.
1016
	 * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods.
1017
	 *                             Singletons don't have their defaults set.
1018
	 */
1019
	public function __construct($filename = null, $isSingleton = false) {
1020
		parent::__construct(array(), $isSingleton);
1021
		$this->ID = -1;
1022
		$this->Filename = $filename;
1023
	}
1024
1025
	/**
1026
	 * Override the parent's exists method becuase the ID is explicitly set to -1 on a cached image we can't use the
1027
	 * default check
1028
	 *
1029
	 * @return bool Whether the cached image exists
1030
	 */
1031
	public function exists() {
1032
		return file_exists($this->getFullPath());
1033
	}
1034
1035
	public function getRelativePath() {
1036
		return $this->getField('Filename');
1037
	}
1038
1039
	/**
1040
	 * Prevent creating new tables for the cached record
1041
	 *
1042
	 * @return false
1043
	 */
1044
	public function requireTable() {
1045
		return false;
1046
	}
1047
1048
	/**
1049
	 * Prevent writing the cached image to the database
1050
	 *
1051
	 * @throws Exception
1052
	 */
1053
	public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
1054
		throw new Exception("{$this->ClassName} can not be written back to the database.");
1055
	}
1056
}
1057