<?php
namespace Grafika\Gd;
use Grafika\DrawingObjectInterface;
use Grafika\EditorInterface;
use Grafika\FilterInterface;
use Grafika\Gd\Helper\GifHelper;
use Grafika\Gd\ImageHash\DifferenceHash;
use Grafika\Grafika;
use Grafika\ImageInterface;
use Grafika\ImageType;
use Grafika\Color;
use Grafika\Position;
final class Editor implements EditorInterface
{
public function apply( &$image, $filter)
{
if ($image->isAnimated()) { return $this;
}
$image = $filter->apply($image);
return $this;
}
public function blend(&$image1, $image2, $type='normal', $opacity = 1.0, $position = 'top-left', $offsetX = 0, $offsetY = 0 ){
$position = new Position($position, $offsetX, $offsetY);
list($offsetX, $offsetY) = $position->getXY($image1->getWidth(), $image1->getHeight(), $image2->getWidth(), $image2->getHeight());
if( ($offsetX >= $image1->getWidth() ) or
($offsetX + $image2->getWidth() <= 0) or
($offsetY >= $image1->getHeight() ) or
($offsetY + $image2->getHeight() <= 0)){
throw new \Exception('Invalid blending. Image 2 is outside the canvas.');
}
$loopStartX = 0;
$canvasStartX = $offsetX;
if($canvasStartX < 0){
$diff = 0 - $canvasStartX;
$loopStartX += $diff;
}
$loopEndX = $image2->getWidth();
$canvasEndX = $offsetX + $image2->getWidth();
if($canvasEndX > $image1->getWidth()){
$diff = $canvasEndX - $image1->getWidth();
$loopEndX -= $diff;
}
$loopStartY = 0;
$canvasStartY = $offsetY;
if($canvasStartY < 0){
$diff = 0 - $canvasStartY;
$loopStartY += $diff;
}
$loopEndY = $image2->getHeight();
$canvasEndY = $offsetY + $image2->getHeight();
if($canvasEndY > $image1->getHeight()){
$diff = $canvasEndY - ($image1->getHeight());
$loopEndY -= $diff;
}
$w = $image1->getWidth();
$h = $image1->getHeight();
$gd1 = $image1->getCore();
$gd2 = $image2->getCore();
$canvas = imagecreatetruecolor( $w, $h );
imagecopy( $canvas, $gd1, 0, 0, 0, 0, $w, $h );
$type = strtolower( $type );
if($type==='normal') {
if ( $opacity !== 1 ) {
$this->opacity($image2, $opacity);
}
imagecopy( $canvas, $gd2, $loopStartX + $offsetX, $loopStartY + $offsetY, 0, 0, $image2->getWidth(), $image2->getHeight());
} else if($type==='multiply'){
$this->_blendMultiply( $canvas, $gd1, $gd2, $loopStartY, $loopEndY, $loopStartX, $loopEndX, $offsetX, $offsetY, $opacity );
} else if($type==='overlay'){
$this->_blendOverlay( $canvas, $gd1, $gd2, $loopStartY, $loopEndY, $loopStartX, $loopEndX, $offsetX, $offsetY, $opacity );
} else if($type==='screen'){
$this->_blendScreen( $canvas, $gd1, $gd2, $loopStartY, $loopEndY, $loopStartX, $loopEndX, $offsetX, $offsetY, $opacity );
} else {
throw new \Exception(sprintf('Invalid blend type "%s".', $type));
}
imagedestroy( $gd1 );
$image1 = new Image(
$canvas,
$image1->getImageFile(),
$w,
$h,
$image1->getType()
);
return $this;
}
public function compare($image1, $image2)
{
if (is_string($image1)) { $image1 = Image::createFromFile($image1);
$this->flatten( $image1 );
}
if (is_string($image2)) { $image2 = Image::createFromFile($image2);
$this->flatten( $image2 );
}
$hash = new DifferenceHash();
$bin1 = $hash->hash($image1, $this);
$bin2 = $hash->hash($image2, $this);
$str1 = str_split($bin1);
$str2 = str_split($bin2);
$distance = 0;
foreach ($str1 as $i => $char) {
if ($char !== $str2[$i]) {
$distance++;
}
}
return $distance;
}
public function crop( &$image, $cropWidth, $cropHeight, $position = 'center', $offsetX = 0, $offsetY = 0)
{
if ($image->isAnimated()) { return $this;
}
if ( 'smart' === $position ) { list( $x, $y ) = $this->_smartCrop( $image, $cropWidth, $cropHeight );
} else {
$position = new Position( $position, $offsetX, $offsetY );
list( $x, $y ) = $position->getXY( $image->getWidth(), $image->getHeight(), $cropWidth, $cropHeight );
}
$newImageResource = imagecreatetruecolor($cropWidth, $cropHeight);
imagecopyresampled(
$newImageResource, $image->getCore(), 0, 0, $x, $y, $cropWidth, $cropHeight, $cropWidth, $cropHeight );
imagedestroy($image->getCore());
$image = new Image(
$newImageResource,
$image->getImageFile(),
$cropWidth,
$cropHeight,
$image->getType()
);
return $this;
}
public function draw( &$image, $drawingObject)
{
if ($image->isAnimated()) { return $this;
}
$image = $drawingObject->draw($image);
return $this;
}
public function equal($image1, $image2)
{
if (is_string($image1)) { $image1 = Image::createFromFile($image1);
$this->flatten( $image1 );
}
if (is_string($image2)) { $image2 = Image::createFromFile($image2);
$this->flatten( $image2 );
}
if ($image1->getWidth() !== $image2->getWidth() or $image1->getHeight() !== $image2->getHeight()) {
return false;
} else {
for ($y = 0; $y < $image1->getHeight(); $y++) {
for ($x = 0; $x < $image1->getWidth(); $x++) {
$rgb1 = imagecolorat($image1->getCore(), $x, $y);
$r1 = ($rgb1 >> 16) & 0xFF;
$g1 = ($rgb1 >> 8) & 0xFF;
$b1 = $rgb1 & 0xFF;
$rgb2 = imagecolorat($image2->getCore(), $x, $y);
$r2 = ($rgb2 >> 16) & 0xFF;
$g2 = ($rgb2 >> 8) & 0xFF;
$b2 = $rgb2 & 0xFF;
if (
$r1 !== $r2 or
$g1 !== $g2 or
$b1 !== $b2
) {
return false;
}
}
}
}
return true;
}
public function fill( &$image, $color, $x = 0, $y = 0)
{
if ($image->isAnimated()) { return $this;
}
list($r, $g, $b, $alpha) = $color->getRgba();
$colorResource = imagecolorallocatealpha($image->getCore(), $r, $g, $b,
$this->gdAlpha($alpha));
imagefill($image->getCore(), $x, $y, $colorResource);
return $this;
}
public function flatten(&$image){
if($image->isAnimated()) {
$old = $image->getCore();
$gift = new GifHelper();
$hex = $gift->encode($image->getBlocks());
$gd = imagecreatefromstring(pack('H*', $hex));
imagedestroy( $old ); $image = new Image(
$gd,
$image->getImageFile(),
$image->getWidth(),
$image->getHeight(),
$image->getType(),
'', false );
}
return $this;
}
public function flip(&$image, $mode){
$image = $this->_flip($image, $mode);
return $this;
}
public function free( &$image )
{
imagedestroy($image->getCore());
return $this;
}
public static function gdAlpha($alpha)
{
$scale = round(127 * $alpha);
return $invert = 127 - $scale;
}
public function isAvailable()
{
if (false === extension_loaded('gd') || false === function_exists('gd_info')) {
return false;
}
if ( ! function_exists('imagerotate')) {
return false;
}
return true;
}
public function opacity( &$image, $opacity )
{
if ($image->isAnimated()) { return $this;
}
$opacity = ($opacity > 1) ? 1 : $opacity;
$opacity = ($opacity < 0) ? 0 : $opacity;
for ($y = 0; $y < $image->getHeight(); $y++) {
for ($x = 0; $x < $image->getWidth(); $x++) {
$rgb = imagecolorat($image->getCore(), $x, $y);
$alpha = ($rgb >> 24) & 0x7F; $r = ($rgb >> 16) & 0xFF;
$g = ($rgb >> 8) & 0xFF;
$b = $rgb & 0xFF;
$reverse = 127 - $alpha;
$reverse = round($reverse * $opacity);
if ($alpha < 127) { imagesetpixel($image->getCore(), $x, $y,
imagecolorallocatealpha($image->getCore(), $r, $g, $b, 127 - $reverse));
}
}
}
return $this;
}
public function open(&$image, $imageFile){
$image = Image::createFromFile( $imageFile );
return $this;
}
public function resize(&$image, $newWidth, $newHeight, $mode = 'fit')
{
switch ($mode) {
case 'exact':
$this->resizeExact($image, $newWidth, $newHeight);
break;
case 'fill':
$this->resizeFill($image, $newWidth, $newHeight);
break;
case 'exactWidth':
$this->resizeExactWidth($image, $newWidth);
break;
case 'exactHeight':
$this->resizeExactHeight($image, $newHeight);
break;
case 'fit':
$this->resizeFit($image, $newWidth, $newHeight);
break;
default:
throw new \Exception(sprintf('Invalid resize mode "%s".', $mode));
}
return $this;
}
public function resizeExact(&$image, $newWidth, $newHeight)
{
$this->_resize($image, $newWidth, $newHeight);
return $this;
}
public function resizeExactHeight(&$image, $newHeight)
{
$width = $image->getWidth();
$height = $image->getHeight();
$ratio = $width / $height;
$resizeHeight = $newHeight;
$resizeWidth = $newHeight * $ratio;
$this->_resize($image, $resizeWidth, $resizeHeight);
return $this;
}
public function resizeExactWidth(&$image, $newWidth)
{
$width = $image->getWidth();
$height = $image->getHeight();
$ratio = $width / $height;
$resizeWidth = $newWidth;
$resizeHeight = round($newWidth / $ratio);
$this->_resize($image, $resizeWidth, $resizeHeight);
return $this;
}
public function resizeFill(&$image, $newWidth, $newHeight)
{
$width = $image->getWidth();
$height = $image->getHeight();
$ratio = $width / $height;
$optimumWidth = $newWidth;
$optimumHeight = round($newWidth / $ratio);
if (($optimumWidth < $newWidth) or ($optimumHeight < $newHeight)) { $optimumWidth = $newHeight * $ratio;
$optimumHeight = $newHeight;
}
$this->_resize($image, $optimumWidth, $optimumHeight);
$this->crop($image, $newWidth, $newHeight);
return $this;
}
public function resizeFit(&$image, $newWidth, $newHeight)
{
$width = $image->getWidth();
$height = $image->getHeight();
$ratio = $width / $height;
$resizeWidth = $newWidth;
$resizeHeight = round($newWidth / $ratio);
if (($resizeWidth > $newWidth) or ($resizeHeight > $newHeight)) { $resizeHeight = $newHeight;
$resizeWidth = $newHeight * $ratio;
}
$this->_resize($image, $resizeWidth, $resizeHeight);
return $this;
}
public function rotate(&$image, $angle, $color = null)
{
if ($image->isAnimated()) { return $this;
}
$color = ($color !== null) ? $color : new Color('#000000');
list($r, $g, $b, $alpha) = $color->getRgba();
$old = $image->getCore();
$new = imagerotate($old, $angle, imagecolorallocatealpha($old, $r, $g, $b, $alpha));
if(false === $new){
throw new \Exception('Error rotating image.');
}
imagedestroy( $old ); $image = new Image( $new, $image->getImageFile(), $image->getWidth(), $image->getHeight(), $image->getType() );
return $this;
}
public function save($image, $file, $type = null, $quality = null, $interlace = false, $permission = 0755)
{
if (null === $type) {
$type = $this->_getImageTypeFromFileName($file); if (ImageType::UNKNOWN === $type) {
$type = $image->getType(); }
}
$targetDir = dirname($file); if (false === is_dir($targetDir)) { if ( ! mkdir($targetDir, $permission, true)) {
throw new \Exception(sprintf('Cannot create %s', $targetDir));
}
}
switch (strtoupper($type)) {
case ImageType::GIF :
if($image->isAnimated()){
$blocks = $image->getBlocks();
$gift = new GifHelper();
$hex = $gift->encode($blocks);
file_put_contents($file, pack('H*', $hex));
} else {
imagegif($image->getCore(), $file);
}
break;
case ImageType::PNG :
imagepng($image->getCore(), $file);
break;
default: $quality = ($quality === null) ? 75 : $quality; $quality = ($quality > 100) ? 100 : $quality;
$quality = ($quality < 0) ? 0 : $quality;
imageinterlace($image->getCore(), $interlace);
imagejpeg($image->getCore(), $file, $quality);
}
return $this;
}
public function text(&$image, $text, $size = 12, $x = 0, $y = 0, $color = null, $font = '', $angle = 0)
{
if ($image->isAnimated()) { return $this;
}
$y += $size;
$color = ($color !== null) ? $color : new Color('#000000');
$font = ($font !== '') ? $font : Grafika::fontsDir() . DIRECTORY_SEPARATOR . 'LiberationSans-Regular.ttf';
list($r, $g, $b, $alpha) = $color->getRgba();
$colorResource = imagecolorallocatealpha(
$image->getCore(),
$r, $g, $b,
$this->gdAlpha($alpha)
);
imagettftext(
$image->getCore(),
$size,
$angle,
$x,
$y,
$colorResource,
$font,
$text
);
return $this;
}
private function _blendMultiply($canvas, $gd1, $gd2, $loopStartY, $loopEndY, $loopStartX, $loopEndX, $offsetX, $offsetY, $opacity){
for ( $y = $loopStartY; $y < $loopEndY; $y++ ) {
for ( $x = $loopStartX; $x < $loopEndX; $x++ ) {
$canvasX = $x + $offsetX;
$canvasY = $y + $offsetY;
$argb1 = imagecolorat( $gd1, $canvasX, $canvasY );
$r1 = ( $argb1 >> 16 ) & 0xFF;
$g1 = ( $argb1 >> 8 ) & 0xFF;
$b1 = $argb1 & 0xFF;
$argb2 = imagecolorat( $gd2, $x, $y );
$a2 = ($argb2 >> 24) & 0x7F; $r2 = ( $argb2 >> 16 ) & 0xFF;
$g2 = ( $argb2 >> 8 ) & 0xFF;
$b2 = $argb2 & 0xFF;
$r3 = round($r1 * $r2 / 255);
$g3 = round($g1 * $g2 / 255);
$b3 = round($b1 * $b2 / 255);
$reverse = 127 - $a2;
$reverse = round($reverse * $opacity);
$argb3 = imagecolorallocatealpha( $canvas, $r3, $g3, $b3, 127 - $reverse );
imagesetpixel( $canvas, $canvasX, $canvasY, $argb3 );
}
}
return $canvas;
}
private function _blendOverlay($canvas, $gd1, $gd2, $loopStartY, $loopEndY, $loopStartX, $loopEndX, $offsetX, $offsetY, $opacity){
for ( $y = $loopStartY; $y < $loopEndY; $y++ ) {
for ( $x = $loopStartX; $x < $loopEndX; $x++ ) {
$canvasX = $x + $offsetX;
$canvasY = $y + $offsetY;
$argb1 = imagecolorat( $gd1, $canvasX, $canvasY );
$r1 = ( $argb1 >> 16 ) & 0xFF;
$g1 = ( $argb1 >> 8 ) & 0xFF;
$b1 = $argb1 & 0xFF;
$argb2 = imagecolorat( $gd2, $x, $y );
$a2 = ($argb2 >> 24) & 0x7F; $r2 = ( $argb2 >> 16 ) & 0xFF;
$g2 = ( $argb2 >> 8 ) & 0xFF;
$b2 = $argb2 & 0xFF;
$r1 /= 255;
$r2 /= 255;
if ($r1 < 0.5) {
$r3 = 2 * ($r1 * $r2);
} else {
$r3 = (1 - (2 *(1-$r1)) * (1-$r2));
}
$g1 /= 255;
$g2 /= 255;
if ($g1 < 0.5) {
$g3 = 2 * ($g1 * $g2);
} else {
$g3 = (1 - (2 *(1-$g1)) * (1-$g2));
}
$b1 /= 255;
$b2 /= 255;
if ($b1 < 0.5) {
$b3 = 2 * ($b1 * $b2);
} else {
$b3 = (1 - (2 *(1-$b1)) * (1-$b2));
}
$reverse = 127 - $a2;
$reverse = round($reverse * $opacity);
$argb3 = imagecolorallocatealpha( $canvas, $r3*255, $g3*255, $b3*255, 127 - $reverse );
imagesetpixel( $canvas, $canvasX, $canvasY, $argb3 );
}
}
return $canvas;
}
private function _entropy($hist){
$entropy = 0;
$hist_size = array_sum($hist['r']) + array_sum($hist['g']) + array_sum($hist['b']);
foreach($hist['r'] as $p){
$p = $p / $hist_size;
$entropy += $p * log($p, 2);
}
foreach($hist['g'] as $p){
$p = $p / $hist_size;
$entropy += $p * log($p, 2);
}
foreach($hist['b'] as $p){
$p = $p / $hist_size;
$entropy += $p * log($p, 2);
}
return $entropy * -1;
}
private function _blendScreen($canvas, $gd1, $gd2, $loopStartY, $loopEndY, $loopStartX, $loopEndX, $offsetX, $offsetY, $opacity){
for ( $y = $loopStartY; $y < $loopEndY; $y++ ) {
for ( $x = $loopStartX; $x < $loopEndX; $x++ ) {
$canvasX = $x + $offsetX;
$canvasY = $y + $offsetY;
$argb1 = imagecolorat( $gd1, $canvasX, $canvasY );
$r1 = ( $argb1 >> 16 ) & 0xFF;
$g1 = ( $argb1 >> 8 ) & 0xFF;
$b1 = $argb1 & 0xFF;
$argb2 = imagecolorat( $gd2, $x, $y );
$a2 = ($argb2 >> 24) & 0x7F; $r2 = ( $argb2 >> 16 ) & 0xFF;
$g2 = ( $argb2 >> 8 ) & 0xFF;
$b2 = $argb2 & 0xFF;
$r3 = 255 - ( ( 255 - $r1 ) * ( 255 - $r2 ) ) / 255;
$g3 = 255 - ( ( 255 - $g1 ) * ( 255 - $g2 ) ) / 255;
$b3 = 255 - ( ( 255 - $b1 ) * ( 255 - $b2 ) ) / 255;
$reverse = 127 - $a2;
$reverse = round($reverse * $opacity);
$argb3 = imagecolorallocatealpha( $canvas, $r3, $g3, $b3, 127 - $reverse );
imagesetpixel( $canvas, $canvasX, $canvasY, $argb3 );
}
}
return $canvas;
}
private function _flip($image, $mode)
{
$old = $image->getCore();
$w = $image->getWidth();
$h = $image->getHeight();
if ($mode === 'h') {
$new = imagecreatetruecolor($w, $h);
for ($x = 0; $x < $w; $x++) {
imagecopy($new, $old, $w - $x - 1, 0, $x, 0, 1, $h);
}
imagedestroy($old); return new Image(
$new,
$image->getImageFile(),
$w,
$h,
$image->getType(),
$image->getBlocks(),
$image->isAnimated()
);
} else if ($mode === 'v') {
$new = imagecreatetruecolor($w, $h);
for ($y = 0; $y < $h; $y++) {
imagecopy($new, $old, 0, $h - $y - 1, 0, $y, $w, 1);
}
imagedestroy($old); return new Image(
$new,
$image->getImageFile(),
$w,
$h,
$image->getType(),
$image->getBlocks(),
$image->isAnimated()
);
} else {
throw new \Exception(sprintf('Unsupported mode "%s"', $mode));
}
}
private function _getImageTypeFromFileName($imageFile)
{
$ext = strtolower((string)pathinfo($imageFile, PATHINFO_EXTENSION));
if ('jpg' === $ext or 'jpeg' === $ext) {
return ImageType::JPEG;
} else if ('gif' === $ext) {
return ImageType::GIF;
} else if ('png' === $ext) {
return ImageType::PNG;
} else if ('wbm' === $ext or 'wbmp' === $ext) {
return ImageType::WBMP;
} else {
return ImageType::UNKNOWN;
}
}
private function _resize(&$image, $newWidth, $newHeight, $targetX = 0, $targetY = 0, $srcX = 0, $srcY = 0)
{
if ($image->isAnimated()) { $gift = new GifHelper();
$blocks = $gift->resize($image->getBlocks(), $newWidth, $newHeight);
$image = new Image(
$image->getCore(),
$image->getImageFile(),
$newWidth,
$newHeight,
$image->getType(),
$blocks,
true
);
} else {
$newImage = Image::createBlank($newWidth, $newHeight);
if (ImageType::PNG === $image->getType()) {
$newImage->fullAlphaMode(true);
}
imagecopyresampled(
$newImage->getCore(),
$image->getCore(),
$targetX,
$targetY,
$srcX,
$srcY,
$newWidth,
$newHeight,
$image->getWidth(),
$image->getHeight()
);
imagedestroy($image->getCore());
$image = new Image(
$newImage->getCore(),
$image->getImageFile(),
$newWidth,
$newHeight,
$image->getType()
);
}
}
private function _smartCrop($oldImage, $cropW, $cropH){
$image = clone $oldImage;
$this->resizeFit($image, 30, 30);
$origW = $oldImage->getWidth();
$origH = $oldImage->getHeight();
$resizeW = $image->getWidth();
$resizeH = $image->getHeight();
$smallCropW = round(($resizeW / $origW) * $cropW);
$smallCropH = round(($resizeH / $origH) * $cropH);
$step = 1;
for($y = 0; $y < $resizeH-$smallCropH; $y+=$step){
for($x = 0; $x < $resizeW-$smallCropW; $x+=$step){
$hist[$x.'-'.$y] = $this->_entropy($image->histogram(array(array($x, $y), array($smallCropW, $smallCropH))));
}
if($resizeW-$smallCropW <= 0){
$hist['0-'.$y] = $this->_entropy($image->histogram(array(array(0, 0), array($smallCropW, $smallCropH))));
}
}
if($resizeH-$smallCropH <= 0){
$hist['0-0'] = $this->_entropy($image->histogram(array(array(0, 0), array($smallCropW, $smallCropH))));
}
asort($hist);
end($hist);
$pos = key($hist); list($x, $y) = explode('-', $pos);
$x = round($x*($origW / $resizeW));
$y = round($y*($origH / $resizeH));
return array($x,$y);
}
}