Completed
Pull Request — master (#6394)
by Ed
10:36
created

ImagickBackend   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 261
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Importance

Changes 0
Metric Value
dl 0
loc 261
c 0
b 0
f 0
rs 8.2769
wmc 41
lcom 2
cbo 4

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A loadFromContainer() 0 7 1
A loadFrom() 0 5 1
A setDefaultQuality() 0 4 1
A writeToStore() 0 12 2
A writeTo() 0 7 2
A setQuality() 0 4 1
D resize() 0 35 10
B resizeRatio() 0 21 5
A resizeByWidth() 0 11 2
A resizeByHeight() 0 11 2
A paddedResize() 0 21 3
A calculateAlphaHex() 0 12 3
B croppedResize() 0 36 5
A crop() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like ImagickBackend 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 ImagickBackend, and based on these observations, apply Extract Interface, too.

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 16 and the first side effect is on line 13.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
namespace SilverStripe\Assets;
4
5
use SilverStripe\Assets\Storage\AssetContainer;
6
use SilverStripe\Assets\Storage\AssetStore;
7
use SilverStripe\Core\Config\Config;
8
use Imagick;
9
use InvalidArgumentException;
10
use ImagickPixel;
11
12
if (!class_exists('Imagick')) {
13
    return;
14
}
15
16
class ImagickBackend extends Imagick implements Image_Backend
17
{
18
19
    /**
20
     * @config
21
     * @var int
22
     */
23
    private static $default_quality = 75;
24
25
    /**
26
     * Create a new backend with the given object
27
     *
28
     * @param AssetContainer $assetContainer Object to load from
29
     */
30
    public function __construct(AssetContainer $assetContainer = null)
31
    {
32
        parent::__construct();
33
34
        if ($assetContainer) {
35
            $this->loadFromContainer($assetContainer);
36
        }
37
    }
38
39
    public function loadFromContainer(AssetContainer $assetContainer)
40
    {
41
        $stream = $assetContainer->getStream();
42
        $this->readImageFile($stream);
43
        fclose($stream);
44
        $this->setDefaultQuality();
45
    }
46
47
    public function loadFrom($path)
48
    {
49
        $this->readImage($path);
50
        $this->setDefaultQuality();
51
    }
52
53
    protected function setDefaultQuality()
54
    {
55
        $this->setQuality(Config::inst()->get('SilverStripe\\Assets\\ImagickBackend', 'default_quality'));
56
    }
57
58
    public function writeToStore(AssetStore $assetStore, $filename, $hash = null, $variant = null, $config = array())
59
    {
60
        // Write to temporary file, taking care to maintain the extension
61
        $path = tempnam(sys_get_temp_dir(), 'imagemagick');
62
        if ($extension = pathinfo($filename, PATHINFO_EXTENSION)) {
63
            $path .= "." . $extension;
64
        }
65
        $this->writeImage($path);
66
        $result = $assetStore->setFromLocalFile($path, $filename, $hash, $variant, $config);
67
        unlink($path);
68
        return $result;
69
    }
70
71
    public function writeTo($path)
72
    {
73
        Filesystem::makeFolder(dirname($path));
74
        if (is_dir(dirname($path))) {
75
            $this->writeImage($path);
76
        }
77
    }
78
79
    public function setQuality($quality)
80
    {
81
        $this->setImageCompressionQuality($quality);
82
    }
83
84
    public function resize($width, $height)
85
    {
86
        if (!$this->valid()) {
87
            return null;
88
        }
89
90
        if ($width < 0 || $height < 0) {
91
            throw new InvalidArgumentException("Image resizing dimensions cannot be negative");
92
        }
93
        if (!$width && !$height) {
94
            throw new InvalidArgumentException("No dimensions given when resizing image");
95
        }
96
        if (!$width) {
97
            throw new InvalidArgumentException("Width not given when resizing image");
98
        }
99
        if (!$height) {
100
            throw new InvalidArgumentException("Height not given when resizing image");
101
        }
102
103
        //use whole numbers, ensuring that size is at least 1x1
104
        $width = max(1, round($width));
105
        $height = max(1, round($height));
106
107
        $geometry = $this->getImageGeometry();
108
109
        // Check that a resize is actually necessary.
110
        if ($width === $geometry["width"] && $height === $geometry["height"]) {
111
            return $this;
112
        }
113
114
        $new = clone $this;
115
        $new->resizeImage($width, $height, self::FILTER_LANCZOS, 1);
116
117
        return $new;
118
    }
119
120
    public function resizeRatio($maxWidth, $maxHeight, $useAsMinimum = false)
121
    {
122
        if (!$this->valid()) {
123
            return null;
124
        }
125
126
        $geometry = $this->getImageGeometry();
127
128
        $widthRatio = $maxWidth / $geometry["width"];
129
        $heightRatio = $maxHeight / $geometry["height"];
130
131
        if ($widthRatio < $heightRatio) {
132
            return $useAsMinimum
133
                ? $this->resizeByHeight($maxHeight)
134
                : $this->resizeByWidth($maxWidth);
135
        } else {
136
            return $useAsMinimum
137
                ? $this->resizeByWidth($maxWidth)
138
                : $this->resizeByHeight($maxHeight);
139
        }
140
    }
141
142
    public function resizeByWidth($width)
143
    {
144
        if (!$this->valid()) {
145
            return null;
146
        }
147
148
        $geometry = $this->getImageGeometry();
149
150
        $heightScale = $width / $geometry["width"];
151
        return $this->resize($width, $heightScale * $geometry["height"]);
152
    }
153
154
    public function resizeByHeight($height)
155
    {
156
        if (!$this->valid()) {
157
            return null;
158
        }
159
160
        $geometry = $this->getImageGeometry();
161
162
        $scale = $height / $geometry["height"];
163
        return $this->resize($scale * $geometry["width"], $height);
164
    }
165
166
    /**
167
     * paddedResize
168
     *
169
     * @param int $width
170
     * @param int $height
171
     * @param string $backgroundColor
172
     * @param int $transparencyPercent
173
     * @return Image_Backend
174
     */
175
    public function paddedResize($width, $height, $backgroundColor = "FFFFFF", $transparencyPercent = 0)
176
    {
177
        if (!$this->valid()) {
178
            return null;
179
        }
180
181
        //keep the % within bounds of 0-100
182
        $transparencyPercent = min(100, max(0, $transparencyPercent));
183
184
        $new = $this->resizeRatio($width, $height);
185
        if ($transparencyPercent) {
186
            $alphaHex = $this->calculateAlphaHex($transparencyPercent);
187
            $new->setImageBackgroundColor("#{$backgroundColor}{$alphaHex}");
188
        } else {
189
            $new->setImageBackgroundColor("#{$backgroundColor}");
190
        }
191
        $w = $new->getImageWidth();
192
        $h = $new->getImageHeight();
193
        $new->extentImage($width, $height, ($w-$width)/2, ($h-$height)/2);
194
        return $new;
195
    }
196
197
    /**
198
     * Convert a percentage (or 'true') to a two char hex code to signifiy the level of an alpha channel
199
     *
200
     * @param $percent
201
     * @return string
202
     */
203
    public function calculateAlphaHex($percent)
204
    {
205
        if ($percent > 100) {
206
            $percent = 100;
207
        }
208
        // unlike GD, this uses 255 instead of 127, and is reversed. Lower = more transparent
209
        $alphaHex = dechex(255 - floor(255 * bcdiv($percent, 100, 2)));
210
        if (strlen($alphaHex) == 1) {
211
            $alphaHex =  '0' .$alphaHex;
212
        }
213
        return $alphaHex;
214
    }
215
216
217
    /**
218
     * croppedResize
219
     *
220
     * @param int $width
221
     * @param int $height
222
     * @return Image_Backend
223
     */
224
    public function croppedResize($width, $height)
225
    {
226
        if (!$this->valid()) {
227
            return null;
228
        }
229
230
        $width = round($width);
231
        $height = round($height);
232
        $geo = $this->getImageGeometry();
233
234
        // Check that a resize is actually necessary.
235
        if ($width == $geo["width"] && $height == $geo["height"]) {
236
            return $this;
237
        }
238
239
        $new = clone $this;
240
        $new->setBackgroundColor(new ImagickPixel('transparent'));
241
242
        if (($geo['width']/$width) < ($geo['height']/$height)) {
243
            $new->cropImage(
244
                $geo['width'],
245
                floor($height*$geo['width']/$width),
246
                0,
247
                ($geo['height'] - ($height*$geo['width']/$width))/2
248
            );
249
        } else {
250
            $new->cropImage(
251
                ceil($width*$geo['height']/$height),
252
                $geo['height'],
253
                ($geo['width'] - ($width*$geo['height']/$height))/2,
254
                0
255
            );
256
        }
257
        $new->thumbnailImage($width, $height, true);
258
        return $new;
259
    }
260
    
261
    /**
262
     * Crop's part of image.
263
     * @param int $top y position of left upper corner of crop rectangle
264
     * @param int $left x position of left upper corner of crop rectangle
265
     * @param int $width rectangle width
266
     * @param int $height rectangle height
267
     * @return Image_Backend
268
     */
269
    public function crop($top, $left, $width, $height)
270
    {
271
        $new = clone $this;
272
        $new->cropImage($width, $height, $left, $top);
273
        
274
        return $new;
275
    }
276
}
277