A PHP Imagick wrapper class that makes image manipulation simpler by making some decisions and by adding new functionality. The `getMainBorderColor()` method is specially interesting. With it you can calculate which color is the most frequent in the lateral borders of an image. I used it to set a background for an area that could possibly be lef…
<?php | |
class SimpleImage { | |
private $img; | |
public function __construct($path) { | |
$path = Filesystem::resolvePath($path); | |
Filesystem::assertExists($path); | |
Filesystem::assertIsFile($path); | |
Filesystem::assertReadable($path); | |
$this->img = new Imagick($path); | |
} | |
/** | |
* Use consciously. | |
* | |
* @return Imagick | |
*/ | |
public function getImagick() { | |
return $this->img; | |
} | |
public function getWidth() { | |
return $this->img->getImageWidth(); | |
} | |
public function getHeight() { | |
return $this->img->getImageHeight(); | |
} | |
public function isVertical() { | |
return $this->getWidth() <= $this->getHeight(); | |
} | |
public function isHorizontal() { | |
return $this->getWidth() >= $this->getHeight(); | |
} | |
public function resize($width, $height) { | |
$filter = Imagick::FILTER_MITCHELL; | |
Enforce::isTrue($this->img->resizeImage($width, $height, $filter, 1)); | |
return $this; | |
} | |
public function crop($width, $height, $left, $top) { | |
Enforce::isTrue($this->img->cropImage($width, $height, $left, $top)); | |
return $this; | |
} | |
public function resizeToWidth($width) { | |
$factor = $width / $this->getWidth(); | |
$height = floor($this->getHeight() * $factor); | |
return $this->resize($width, $height); | |
} | |
public function resizeToBoundingBox($bound_width, $bound_height) { | |
$w = $this->getWidth(); | |
$h = $this->getHeight(); | |
if ($w <= $bound_width && $h <= $bound_height) { | |
return $this; | |
} | |
$factor = min($bound_width / $w, $bound_height / $h); | |
return $this->resize(floor($factor * $w), floor($factor * $h)); | |
} | |
public function cropThumbnailImage($width, $height) { | |
$this->img->cropThumbnailImage($width, $height); | |
return $this; | |
} | |
public function cropCenter($width, $height) { | |
return $this->crop( | |
$width, | |
$height, | |
floor(($this->getWidth() - $width) / 2), | |
floor(($this->getHeight() - $height) / 2)); | |
} | |
/** | |
* @param arr $c A normalized RGB color | |
* @return arr | |
*/ | |
private static function rgbToYuv($c) { | |
$r = $c['r']; | |
$g = $c['g']; | |
$b = $c['b']; | |
return array( | |
/* $y = */ 0.299 * $r + 0.587 * $g + 0.114 * $b, | |
/* $u = */ -0.147 * $r - 0.289 * $g + 0.436 * $b, | |
/* $v = */ 0.615 * $r - 0.515 * $g - 0.100 * $b); | |
} | |
private static function clamp($w) { | |
$v = (int) ($w * 255); | |
if ($v < 0) { | |
$v = 0; | |
} else if ($v > 255) { | |
$v = 255; | |
} | |
return $v; | |
} | |
/** | |
* @param arr $c | |
* @return arr RGB components in [0, 255] | |
*/ | |
private static function yuvToRgb($c) { | |
list($y, $u, $v) = $c; | |
$r = self::clamp($y + 1.13983 * $v); | |
$g = self::clamp($y - 0.39465 * $u - 0.5806 * $v); | |
$b = self::clamp($y + 2.03211 * $u); | |
return array( | |
'r' => $r, | |
'g' => $g, | |
'b' => $b); | |
} | |
private static function yuvColorDistance($c1, $c2) { | |
return sqrt($c1[0] * $c2[0] + $c1[1] * $c2[1] + $c1[2] * $c2[2]); | |
} | |
private static function yuvIsSimilar($c1, $c2) { | |
// threshold distance: 0.1 | |
return $c1[0] * $c2[0] + $c1[1] * $c2[1] + $c1[2] * $c2[2] < 0.01; | |
} | |
private static function rgbColorToHex($c) { | |
$r_hex = dechex($c['r']); | |
$g_hex = dechex($c['g']); | |
$b_hex = dechex($c['b']); | |
$c_str = | |
($c['r'] < 16 ? '0'.$r_hex : $r_hex). | |
($c['g'] < 16 ? '0'.$g_hex : $g_hex). | |
($c['b'] < 16 ? '0'.$b_hex : $b_hex); | |
return $c_str; | |
} | |
public function getMainBorderColor() { | |
$img = clone $this->img; | |
$img->adaptiveResizeImage(0, 40); | |
$pixel_colors = array(); // YUV color for each pixel | |
$first_col_it = | |
$img->getPixelRegionIterator(0, 0, 1, $img->getImageWidth()); | |
foreach ($first_col_it as $col) { | |
foreach ($col as $pixel) { | |
$pixel_colors[] = self::rgbToYuv($pixel->getColor(true)); | |
} | |
} | |
$first_col_it->destroy(); | |
$last_col_it = $img->getPixelRegionIterator( | |
$img->getImageWidth() - 1, 0, 1, $img->getImageWidth()); | |
foreach ($last_col_it as $col) { | |
foreach ($col as $pixel) { | |
$pixel_colors[] = self::rgbToYuv($pixel->getColor(true)); | |
} | |
} | |
$last_col_it->destroy(); | |
$img->destroy(); | |
$color_groups = array(array($pixel_colors[0])); | |
for ($i = 1; $i < count($pixel_colors); $i++) { | |
$color = $pixel_colors[$i]; | |
foreach ($color_groups as &$group) { | |
foreach ($group as $color_in_group) { | |
if (self::yuvIsSimilar($color, $color_in_group)) { | |
$group[] = $color; | |
break 2; | |
} | |
} | |
} | |
$color_groups[] = array($color); | |
} | |
$max_group_size = 1; | |
$max_group = $color_groups[0]; | |
foreach ($color_groups as $g) { | |
$g_size = count($g); | |
if ($g_size > $max_group_size) { | |
$max_group_size = $g_size; | |
$max_group = $g; | |
} | |
} | |
$mean = array(0.0, 0.0, 0.0); | |
foreach ($max_group as $c) { | |
$mean[0] += $c[0]; | |
$mean[1] += $c[1]; | |
$mean[2] += $c[2]; | |
} | |
$mean[0] /= $max_group_size; | |
$mean[1] /= $max_group_size; | |
$mean[2] /= $max_group_size; | |
return self::rgbColorToHex(self::yuvToRgb($mean)); | |
} | |
public function save($path, $format = null) { | |
$path = Filesystem::resolvePath($path); | |
$dir = dirname($path); | |
Filesystem::assertExists($dir); | |
Filesystem::assertIsDirectory($dir); | |
if ($format) { | |
$this->img->setImageFormat($format); | |
} | |
$this->img->writeImage($path); | |
return $this; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment