Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like GDBackend 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 GDBackend, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
7 | class GDBackend extends Object implements Image_Backend { |
||
8 | protected $gd, $width, $height; |
||
9 | protected $quality; |
||
10 | protected $interlace; |
||
11 | protected $cache, $cacheKey, $manipulation; |
||
12 | |||
13 | /** |
||
14 | * @config |
||
15 | * @var integer |
||
16 | */ |
||
17 | private static $default_quality = 75; |
||
18 | |||
19 | /** |
||
20 | * @config |
||
21 | * @var integer |
||
22 | */ |
||
23 | private static $image_interlace = 0; |
||
24 | |||
25 | /** |
||
26 | * Set the default image quality. |
||
27 | * |
||
28 | * @deprecated 4.0 Use the "GDBackend.default_quality" config setting instead |
||
29 | * @param quality int A number from 0 to 100, 100 being the best quality. |
||
30 | */ |
||
31 | public static function set_default_quality($quality) { |
||
37 | |||
38 | public function __construct($filename = null, $args = array()) { |
||
39 | // If we're working with image resampling, things could take a while. Bump up the time-limit |
||
40 | increase_time_limit_to(300); |
||
41 | |||
42 | $this->cache = SS_Cache::factory('GDBackend_Manipulations'); |
||
43 | |||
44 | if($filename && is_readable($filename)) { |
||
45 | $this->cacheKey = md5(implode('_', array($filename, filemtime($filename)))); |
||
46 | $this->manipulation = implode('|', $args); |
||
47 | |||
48 | $cacheData = unserialize($this->cache->load($this->cacheKey)); |
||
49 | $cacheData = ($cacheData !== false) ? $cacheData : array(); |
||
50 | |||
51 | if ($this->imageAvailable($filename, $this->manipulation)) { |
||
52 | $cacheData[$this->manipulation] = true; |
||
53 | $this->cache->save(serialize($cacheData), $this->cacheKey); |
||
54 | |||
55 | // We use getimagesize instead of extension checking, because sometimes extensions are wrong. |
||
56 | list($width, $height, $type, $attr) = getimagesize($filename); |
||
57 | switch($type) { |
||
58 | case 1: |
||
59 | if(function_exists('imagecreatefromgif')) |
||
60 | $this->setImageResource(imagecreatefromgif($filename)); |
||
61 | break; |
||
62 | case 2: |
||
63 | if(function_exists('imagecreatefromjpeg')) |
||
64 | $this->setImageResource(imagecreatefromjpeg($filename)); |
||
65 | break; |
||
66 | case 3: |
||
67 | if(function_exists('imagecreatefrompng')) { |
||
68 | $img = imagecreatefrompng($filename); |
||
69 | imagesavealpha($img, true); // save alphablending setting (important) |
||
70 | $this->setImageResource($img); |
||
71 | } |
||
72 | break; |
||
73 | } |
||
74 | } |
||
75 | } |
||
76 | |||
77 | parent::__construct(); |
||
78 | |||
79 | $this->quality = $this->config()->default_quality; |
||
80 | $this->interlace = $this->config()->image_interlace; |
||
81 | } |
||
82 | |||
83 | public function setImageResource($resource) { |
||
84 | $this->gd = $resource; |
||
85 | $this->width = imagesx($resource); |
||
86 | $this->height = imagesy($resource); |
||
87 | } |
||
88 | |||
89 | /** |
||
90 | * @deprecated |
||
91 | */ |
||
92 | public function setGD($gd) { |
||
96 | |||
97 | public function getImageResource() { |
||
100 | |||
101 | /** |
||
102 | * @deprecated |
||
103 | */ |
||
104 | public function getGD() { |
||
108 | |||
109 | /** |
||
110 | * @param string $filename |
||
111 | * @param string $manipulation |
||
112 | * @return boolean |
||
113 | */ |
||
114 | public function imageAvailable($filename, $manipulation) { |
||
117 | |||
118 | /** |
||
119 | * Check if we've got enough memory available for resampling this image. This check is rough, |
||
120 | * so it will not catch all images that are too large - it also won't work accurately on large, |
||
121 | * animated GIFs as bits per pixel can't be calculated for an animated GIF with a global color |
||
122 | * table. |
||
123 | * |
||
124 | * @param string $filename |
||
125 | * @return boolean |
||
126 | */ |
||
127 | public function checkAvailableMemory($filename) { |
||
147 | |||
148 | /** |
||
149 | * Check if this image has previously crashed GD when attempting to open it - if it's opened |
||
150 | * successfully, the manipulation's cache key is removed. |
||
151 | * |
||
152 | * @param string $filename |
||
153 | * @return boolean |
||
154 | */ |
||
155 | public function failedResample($filename, $manipulation) { |
||
159 | |||
160 | /** |
||
161 | * Set the image quality, used when saving JPEGs. |
||
162 | */ |
||
163 | public function setQuality($quality) { |
||
166 | |||
167 | /** |
||
168 | * Resize an image to cover the given width/height completely, and crop off any overhanging edges. |
||
169 | */ |
||
170 | public function croppedResize($width, $height) { |
||
171 | if(!$this->gd) return; |
||
172 | |||
173 | $width = round($width); |
||
174 | $height = round($height); |
||
175 | |||
176 | // Check that a resize is actually necessary. |
||
177 | if ($width == $this->width && $height == $this->height) { |
||
178 | return $this; |
||
179 | } |
||
180 | |||
181 | $newGD = imagecreatetruecolor($width, $height); |
||
182 | |||
183 | // Preserves transparency between images |
||
184 | imagealphablending($newGD, false); |
||
185 | imagesavealpha($newGD, true); |
||
186 | |||
187 | $destAR = $width / $height; |
||
188 | if ($this->width > 0 && $this->height > 0 ){ |
||
189 | // We can't divide by zero theres something wrong. |
||
190 | |||
191 | $srcAR = $this->width / $this->height; |
||
192 | |||
193 | // Destination narrower than the source |
||
194 | if($destAR < $srcAR) { |
||
195 | $srcY = 0; |
||
196 | $srcHeight = $this->height; |
||
197 | |||
198 | $srcWidth = round( $this->height * $destAR ); |
||
199 | $srcX = round( ($this->width - $srcWidth) / 2 ); |
||
200 | |||
201 | // Destination shorter than the source |
||
202 | } else { |
||
203 | $srcX = 0; |
||
204 | $srcWidth = $this->width; |
||
205 | |||
206 | $srcHeight = round( $this->width / $destAR ); |
||
207 | $srcY = round( ($this->height - $srcHeight) / 2 ); |
||
208 | } |
||
209 | |||
210 | imagecopyresampled($newGD, $this->gd, 0,0, $srcX, $srcY, $width, $height, $srcWidth, $srcHeight); |
||
211 | } |
||
212 | $output = clone $this; |
||
213 | $output->setImageResource($newGD); |
||
214 | return $output; |
||
215 | } |
||
216 | |||
217 | /** |
||
218 | * Resizes the image to fit within the given region. |
||
219 | * Behaves similarly to paddedResize but without the padding. |
||
220 | * @todo This method isn't very efficent |
||
221 | */ |
||
222 | public function fittedResize($width, $height) { |
||
223 | $gd = $this->resizeByHeight($height); |
||
224 | if($gd->width > $width) $gd = $gd->resizeByWidth($width); |
||
225 | return $gd; |
||
226 | } |
||
227 | |||
228 | /** |
||
229 | * hasImageResource |
||
230 | * |
||
231 | * @return boolean |
||
232 | */ |
||
233 | public function hasImageResource() { |
||
236 | |||
237 | /** |
||
238 | * @deprecated |
||
239 | */ |
||
240 | public function hasGD() { |
||
241 | Deprecation::notice('4.0', 'GD::hasImageResource instead', |
||
242 | Deprecation::SCOPE_CLASS); |
||
243 | return $this->hasImageResource(); |
||
244 | } |
||
245 | |||
246 | |||
247 | /** |
||
248 | * Resize an image, skewing it as necessary. |
||
249 | */ |
||
250 | public function resize($width, $height) { |
||
251 | if(!$this->gd) return; |
||
252 | |||
253 | if($width < 0 || $height < 0) throw new InvalidArgumentException("Image resizing dimensions cannot be negative"); |
||
254 | if(!$width && !$height) throw new InvalidArgumentException("No dimensions given when resizing image"); |
||
255 | if(!$width) throw new InvalidArgumentException("Width not given when resizing image"); |
||
256 | if(!$height) throw new InvalidArgumentException("Height not given when resizing image"); |
||
257 | |||
258 | //use whole numbers, ensuring that size is at least 1x1 |
||
259 | $width = max(1, round($width)); |
||
260 | $height = max(1, round($height)); |
||
261 | |||
262 | // Check that a resize is actually necessary. |
||
263 | if ($width == $this->width && $height == $this->height) { |
||
264 | return $this; |
||
265 | } |
||
266 | |||
267 | |||
268 | $newGD = imagecreatetruecolor($width, $height); |
||
269 | |||
270 | // Preserves transparency between images |
||
271 | imagealphablending($newGD, false); |
||
272 | imagesavealpha($newGD, true); |
||
273 | |||
274 | imagecopyresampled($newGD, $this->gd, 0,0, 0, 0, $width, $height, $this->width, $this->height); |
||
275 | |||
276 | $output = clone $this; |
||
277 | $output->setImageResource($newGD); |
||
278 | return $output; |
||
279 | } |
||
280 | |||
281 | /** |
||
282 | * Rotates image by given angle. |
||
283 | * |
||
284 | * @param angle |
||
285 | * |
||
286 | * @return GD |
||
287 | */ |
||
288 | |||
289 | public function rotate($angle) { |
||
290 | if(!$this->gd) return; |
||
291 | |||
292 | if(function_exists("imagerotate")) { |
||
293 | $newGD = imagerotate($this->gd, $angle,0); |
||
294 | } else { |
||
295 | //imagerotate is not included in PHP included in Ubuntu |
||
296 | $newGD = $this->rotatePixelByPixel($angle); |
||
297 | } |
||
298 | $output = clone $this; |
||
299 | $output->setImageResource($newGD); |
||
300 | return $output; |
||
301 | } |
||
302 | |||
303 | /** |
||
304 | * Rotates image by given angle. It's slow because makes it pixel by pixel rather than |
||
305 | * using built-in function. Used when imagerotate function is not available(i.e. Ubuntu) |
||
306 | * |
||
307 | * @param angle |
||
308 | * |
||
309 | * @return GD |
||
310 | */ |
||
311 | |||
312 | public function rotatePixelByPixel($angle) { |
||
313 | $sourceWidth = imagesx($this->gd); |
||
314 | $sourceHeight = imagesy($this->gd); |
||
315 | if ($angle == 180) { |
||
316 | $destWidth = $sourceWidth; |
||
317 | $destHeight = $sourceHeight; |
||
318 | } else { |
||
319 | $destWidth = $sourceHeight; |
||
320 | $destHeight = $sourceWidth; |
||
321 | } |
||
322 | $rotate=imagecreatetruecolor($destWidth,$destHeight); |
||
323 | imagealphablending($rotate, false); |
||
324 | for ($x = 0; $x < ($sourceWidth); $x++) { |
||
325 | for ($y = 0; $y < ($sourceHeight); $y++) { |
||
326 | $color = imagecolorat($this->gd, $x, $y); |
||
327 | switch ($angle) { |
||
328 | case 90: |
||
329 | imagesetpixel($rotate, $y, $destHeight - $x - 1, $color); |
||
330 | break; |
||
331 | case 180: |
||
332 | imagesetpixel($rotate, $destWidth - $x - 1, $destHeight - $y - 1, $color); |
||
333 | break; |
||
334 | case 270: |
||
335 | imagesetpixel($rotate, $destWidth - $y - 1, $x, $color); |
||
336 | break; |
||
337 | default: $rotate = $this->gd; |
||
338 | }; |
||
339 | } |
||
340 | } |
||
341 | return $rotate; |
||
342 | } |
||
343 | |||
344 | |||
345 | /** |
||
346 | * Crop's part of image. |
||
347 | * |
||
348 | * @param top y position of left upper corner of crop rectangle |
||
349 | * @param left x position of left upper corner of crop rectangle |
||
350 | * @param width rectangle width |
||
351 | * @param height rectangle height |
||
352 | * |
||
353 | * @return GD |
||
354 | */ |
||
355 | |||
356 | public function crop($top, $left, $width, $height) { |
||
357 | $newGD = imagecreatetruecolor($width, $height); |
||
358 | |||
359 | // Preserve alpha channel between images |
||
360 | imagealphablending($newGD, false); |
||
361 | imagesavealpha($newGD, true); |
||
362 | |||
363 | imagecopyresampled($newGD, $this->gd, 0, 0, $left, $top, $width, $height, $width, $height); |
||
364 | |||
365 | $output = clone $this; |
||
366 | $output->setImageResource($newGD); |
||
367 | return $output; |
||
368 | } |
||
369 | |||
370 | /** |
||
371 | * Method return width of image. |
||
372 | * |
||
373 | * @return integer width. |
||
374 | */ |
||
375 | public function getWidth() { |
||
378 | |||
379 | /** |
||
380 | * Method return height of image. |
||
381 | * |
||
382 | * @return integer height |
||
383 | */ |
||
384 | |||
385 | public function getHeight() { |
||
388 | |||
389 | /** |
||
390 | * Resize an image by width. Preserves aspect ratio. |
||
391 | */ |
||
392 | public function resizeByWidth( $width ) { |
||
396 | |||
397 | /** |
||
398 | * Resize an image by height. Preserves aspect ratio |
||
399 | */ |
||
400 | public function resizeByHeight( $height ) { |
||
404 | |||
405 | /** |
||
406 | * Resize the image by preserving aspect ratio. By default, it will keep the image inside the maxWidth |
||
407 | * and maxHeight. Passing useAsMinimum will make the smaller dimension equal to the maximum corresponding dimension |
||
408 | */ |
||
409 | public function resizeRatio( $maxWidth, $maxHeight, $useAsMinimum = false ) { |
||
410 | |||
411 | $widthRatio = $maxWidth / $this->width; |
||
412 | $heightRatio = $maxHeight / $this->height; |
||
413 | |||
414 | View Code Duplication | if( $widthRatio < $heightRatio ) |
|
415 | return $useAsMinimum ? $this->resizeByHeight( $maxHeight ) : $this->resizeByWidth( $maxWidth ); |
||
416 | else |
||
417 | return $useAsMinimum ? $this->resizeByWidth( $maxWidth ) : $this->resizeByHeight( $maxHeight ); |
||
418 | } |
||
419 | |||
420 | public static function color_web2gd($image, $webColor) { |
||
421 | if(substr($webColor,0,1) == "#") $webColor = substr($webColor,1); |
||
422 | $r = hexdec(substr($webColor,0,2)); |
||
423 | $g = hexdec(substr($webColor,2,2)); |
||
424 | $b = hexdec(substr($webColor,4,2)); |
||
425 | |||
426 | return imagecolorallocate($image, $r, $g, $b); |
||
427 | |||
428 | } |
||
429 | |||
430 | /** |
||
431 | * Resize to fit fully within the given box, without resizing. Extra space left around |
||
432 | * the image will be padded with the background color. |
||
433 | * @param width |
||
434 | * @param height |
||
435 | * @param backgroundColour |
||
436 | */ |
||
437 | public function paddedResize($width, $height, $backgroundColor = "FFFFFF") { |
||
438 | if(!$this->gd) return; |
||
439 | $width = round($width); |
||
440 | $height = round($height); |
||
441 | |||
442 | // Check that a resize is actually necessary. |
||
443 | if ($width == $this->width && $height == $this->height) { |
||
444 | return $this; |
||
445 | } |
||
446 | |||
447 | $newGD = imagecreatetruecolor($width, $height); |
||
448 | |||
449 | // Preserves transparency between images |
||
450 | imagealphablending($newGD, false); |
||
451 | imagesavealpha($newGD, true); |
||
452 | |||
453 | $bg = GD::color_web2gd($newGD, $backgroundColor); |
||
454 | imagefilledrectangle($newGD, 0, 0, $width, $height, $bg); |
||
455 | |||
456 | $destAR = $width / $height; |
||
457 | if ($this->width > 0 && $this->height > 0) { |
||
458 | // We can't divide by zero theres something wrong. |
||
459 | |||
460 | $srcAR = $this->width / $this->height; |
||
461 | |||
462 | // Destination narrower than the source |
||
463 | if($destAR > $srcAR) { |
||
464 | $destY = 0; |
||
465 | $destHeight = $height; |
||
466 | |||
467 | $destWidth = round( $height * $srcAR ); |
||
468 | $destX = round( ($width - $destWidth) / 2 ); |
||
469 | |||
470 | // Destination shorter than the source |
||
471 | } else { |
||
472 | $destX = 0; |
||
473 | $destWidth = $width; |
||
474 | |||
475 | $destHeight = round( $width / $srcAR ); |
||
476 | $destY = round( ($height - $destHeight) / 2 ); |
||
477 | } |
||
478 | |||
479 | imagecopyresampled($newGD, $this->gd, |
||
480 | $destX, $destY, 0, 0, |
||
481 | $destWidth, $destHeight, $this->width, $this->height); |
||
482 | } |
||
483 | $output = clone $this; |
||
484 | $output->setImageResource($newGD); |
||
485 | return $output; |
||
486 | } |
||
487 | |||
488 | /** |
||
489 | * Make the image greyscale |
||
490 | * $rv = red value, defaults to 38 |
||
491 | * $gv = green value, defaults to 36 |
||
492 | * $bv = blue value, defaults to 26 |
||
493 | * Based (more or less entirely, with changes for readability) on code from |
||
494 | * http://www.teckis.com/scriptix/thumbnails/teck.html |
||
495 | */ |
||
496 | public function greyscale($rv=38, $gv=36, $bv=26) { |
||
497 | $width = $this->width; |
||
498 | $height = $this->height; |
||
499 | $newGD = imagecreatetruecolor($this->width, $this->height); |
||
500 | |||
501 | // Preserves transparency between images |
||
502 | imagealphablending($newGD, false); |
||
503 | imagesavealpha($newGD, true); |
||
504 | |||
505 | $rt = $rv + $bv + $gv; |
||
506 | $rr = ($rv == 0) ? 0 : 1/($rt/$rv); |
||
507 | $br = ($bv == 0) ? 0 : 1/($rt/$bv); |
||
508 | $gr = ($gv == 0) ? 0 : 1/($rt/$gv); |
||
509 | for($dy = 0; $dy < $height; $dy++) { |
||
510 | for($dx = 0; $dx < $width; $dx++) { |
||
511 | $pxrgb = imagecolorat($this->gd, $dx, $dy); |
||
512 | $heightgb = ImageColorsforIndex($this->gd, $pxrgb); |
||
513 | $newcol = ($rr*$heightgb['red']) + ($br*$heightgb['blue']) + ($gr*$heightgb['green']); |
||
514 | $setcol = ImageColorAllocateAlpha($newGD, $newcol, $newcol, $newcol, $heightgb['alpha']); |
||
515 | imagesetpixel($newGD, $dx, $dy, $setcol); |
||
516 | } |
||
517 | } |
||
518 | |||
519 | $output = clone $this; |
||
520 | $output->setImageResource($newGD); |
||
521 | return $output; |
||
522 | } |
||
523 | |||
524 | public function makeDir($dirname) { |
||
528 | |||
529 | public function writeTo($filename) { |
||
530 | $this->makeDir(dirname($filename)); |
||
531 | |||
532 | if($filename) { |
||
533 | if(file_exists($filename)) list($width, $height, $type, $attr) = getimagesize($filename); |
||
534 | |||
535 | if(file_exists($filename)) unlink($filename); |
||
536 | |||
537 | $ext = strtolower(substr($filename, strrpos($filename,'.')+1)); |
||
538 | if(!isset($type)) switch($ext) { |
||
539 | case "gif": $type = IMAGETYPE_GIF; break; |
||
540 | case "jpeg": case "jpg": case "jpe": $type = IMAGETYPE_JPEG; break; |
||
541 | default: $type = IMAGETYPE_PNG; break; |
||
567 | |||
568 | /** |
||
569 | * @param Image $frontend |
||
570 | * @return void |
||
571 | */ |
||
572 | public function onBeforeDelete($frontend) { |
||
580 | |||
581 | } |
||
582 | |||
599 |
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.