pix0r
pix0r

Reputation: 31290

Crop or mask an image into a circle

What is the best way to crop or mask an image into a circular shape, using either ImageMagick or GD libraries? (Note, solution exists on "other" Q&A sites, but not StackOverflow)

Upvotes: 18

Views: 24339

Answers (7)

Rejown Ahmed
Rejown Ahmed

Reputation: 84

For those who are using intervention/image v3 in Laravel since the mask method has been removed, we can use a custom class to do the job.

(Note:- Raw php developers can also use this class provided that GD extension is enabled)

<?php

namespace App\Helpers\Image;

class CircleImage
{
    public $img;

    public $width;

    public $height;
    public $minSize;

    public function __construct($img = null)
    {
        if (!empty($img)) {
            $this->img = imagecreatefromstring($img);
            $this->width = imagesx($this->img); // Image original width
            $this->height = imagesy($this->img); // Image original height
            // Get the minimum size
            // This will help cut the image in a circular shape
            $this->minSize = min($this->width, $this->height);
        }
    }

    public function make(): string
    {
        $radius = $this->minSize / 2;
        // First we crop the image from center
        $cropped = imagecrop($this->img, [
            // Calculation summary:
            // here width/2 gives us the center point, suppose our image width is 200,
            // Then the center point is 100, now we want our cropping to start
            // at negative $radius from 100 and end at positive $radius from 100,
            // So that it crops from the center part into a square with the
            // Diameter same as minSize (Same for height)
            "x" => $this->width / 2 - $radius,
            "y" => $this->height / 2 - $radius,
            "width" => $this->minSize, // Diameter
            "height" => $this->minSize, // Diameter
        ]);

        if ($cropped !== false) { // in case a new image object was returned
            imagedestroy($this->img);    // we destroy the original image
            $this->img = $cropped;       // and assign the cropped image

        } else {
            throw new \Exception("Failed to crop the image!", 500);

        }

        // Now create the circular mask
        $mask = imagecreatetruecolor($this->minSize, $this->minSize);
        $black = imagecolorallocate($mask, 0, 0, 0);
        $magenta = imagecolorallocate($mask, 255, 0, 255);

        // Fill with magenta color
        imagefill($mask, 0, 0, $magenta);

        // Create a black circle in the center
        imagefilledellipse(
            $mask,
            $radius,
            $radius,
            $this->minSize,
            $this->minSize,
            $black
        );

        // Now make the black circle part transparent
        imagecolortransparent($mask, $black);

        // Merge the two images so that only the transparent
        // part shows the image and other parts become magenta solid
        imagecopymerge(
            $this->img,
            $mask,
            0,
            0,
            0,
            0,
            $this->minSize,
            $this->minSize,
            100
        );

        // Now make the magenta part transparent
        // It now only keeps the center transparent(prev=black) part
        // of the original image visible
        imagecolortransparent($this->img, $magenta);

        // Destroy the mask
        imagedestroy($mask);

        return $this->render();
    }

    public function render(): string
    {
        // Get the string content
        // Of the generated image & return
        ob_start();
        imagepng($this->img);
        $imagedata = ob_get_clean();

        return $imagedata;

    }
}

###Usage:-

// First we scale the image to a smaller size, otherwise the circular crop may time out and get the string value
$image = InterventionImage::read($image)->scale(300, null)->encodeByMediaType(type: "image/png", quality: 90)->toString();

// Now simply Crop the image as a circle with our helper class
$circleImage = new CircleImage($image);
$image = $circleImage->make();

// It returns the png string image content, either convert to base64 or simply put to the storage Like below:-
// Storage: Storage::put("picture/avatar.png", $image);
// base64_encode($image);

Upvotes: 0

luvzfootball
luvzfootball

Reputation: 760

Here is solution based on masks with GD called imageRemoveOuterCircle:

// From https://stackoverflow.com/a/23215738/2590508
function hexColorAllocate($im,$hex){
    $hex = ltrim($hex,'#');
    $r = hexdec(substr($hex,0,2));
    $g = hexdec(substr($hex,2,2));
    $b = hexdec(substr($hex,4,2));
    return imagecolorallocate($im, $r, $g, $b);
}

function imageRemoveOuterCircle(&$image,$width=null,$height=null){
    // 2 arbitrary colors for transparency ; can be redefined if needed
    $transparentColor1="8d5ca4";
    $transparentColor2="8d5ca5";

    if(is_null($width)){
        $width=imagesx($image);
    }
    if(is_null($height)){
        $height=imagesy($image);
    }

    $mask=imagecreatetruecolor($width, $height);
    imagefilledrectangle(
        $mask,
        0,
        0,
        $width,
        $height,
        hexColorAllocate($mask,$transparentColor1)
    );
    imagefilledellipse(
        $mask,
        $width/2,
        $height/2,
        $width,
        $height,
        hexColorAllocate($mask,$transparentColor2)
    );
    imagecolortransparent($mask,hexColorAllocate($mask,$transparentColor2));
    imagecopy(
        $image,
        $mask,
        0,
        0,
        0,
        0,
        $width,
        $height
    );
    imagedestroy($mask);
    imagecolortransparent($image,hexColorAllocate($image,$transparentColor1));
}

Upvotes: 0

Ferhad Konar
Ferhad Konar

Reputation: 444

You can use this function if you want to circular the corners of an image. To make the circle image, you need to set $Radius to 50%; If the $source_url is a square, the output will be the circle, otherwise it will be oval.The output will be a PNG file with transparent background.

public function Crop_ByRadius($source_url,$destination_url="",$Radius="0px" ,$Keep_SourceFile = TRUE){

    /*
        Output File is png, Because for crop we need transparent color

        if success :: this function returns url of Created File
        if Fial :: returns FALSE

        $Radius Input Examples :: 
                            100     => 100px                    
                            100px   => 100px                    
                            50%     => 50%                  
    */

    if( !file_exists($source_url) || $Radius == NULL )
        return FALSE;

    if( $destination_url == NULL || $destination_url == "" ) $destination_url = $source_url;


    $PathInfo = pathinfo($destination_url);
    $destination_url = $PathInfo['dirname'].DIRECTORY_SEPARATOR.$PathInfo['filename'].".png";

    $ImageInfo = getimagesize($source_url);
    $w = $ImageInfo[0];
    $h = $ImageInfo[1];
    $mime = $ImageInfo['mime'];

    if( $mime != "image/jpeg" && $mime != "image/jpg" && $mime != "image/png")
        return FALSE;

    if( strpos($Radius,"%") !== FALSE ){
        //$Radius by Cent
        $Radius = intval( str_replace("%","",$Radius) );
        $Smallest_Side = $w <= $h ? $w : $h;
        $Radius = $Smallest_Side * $Radius / 100; 

    }else{
        $Radius = strtolower($Radius);
        $Radius = str_replace("px","",$Radius);
    }

    $Radius = is_numeric($Radius) ? intval($Radius) : 0;

    if( $Radius == 0 ) return FALSE;

    $src = imagecreatefromstring(file_get_contents($source_url));
    $newpic = imagecreatetruecolor($w,$h);
    imagealphablending($newpic,false);
    $transparent = imagecolorallocatealpha($newpic, 0, 0, 0, 127);
    //$transparent = imagecolorallocatealpha($newpic, 255, 0, 0, 0);//RED For Test

    $r = $Radius / 2;

    /********************** Pixel step config ********************************/

    $Pixel_Step_def = 0.4;//smaller step take longer time! if set $Pixel_Step=0.1 result is better than  $Pixel_Step=1 but it take longer time!

    //We select the pixels we are sure are in range, to Take up the bigger steps and shorten the processing time

    $Sure_x_Start = $Radius +1;
    $Sure_x_End = $w - $Radius -1;
    $Sure_y_Start = $Radius +1;
    $Sure_y_End = $h - $Radius -1;
    if( $w <= $h ){
        //We want to use the larger side to make processing shorter
        $Use_x_Sure = FALSE;
        $Use_y_Sure = TRUE;
    }else{
        $Use_x_Sure = TRUE;
        $Use_y_Sure = FALSE;
    }
    /********************** Pixel step config END********************************/


    $Pixel_Step = $Pixel_Step_def;
    for( $x=0; $x < $w ; $x+=$Pixel_Step ){

        if( $Use_x_Sure && $x > $Sure_x_Start && $x < $Sure_x_End ) $Pixel_Step = 1;else $Pixel_Step = $Pixel_Step_def; 

        for( $y=0; $y < $h ; $y+=$Pixel_Step){
            if( $Use_y_Sure && $y > $Sure_y_Start && $y < $Sure_y_End ) $Pixel_Step = 1;else $Pixel_Step = $Pixel_Step_def; 

            $c = imagecolorat($src,$x,$y);

            $_x = ($x - $Radius) /2;
            $_y = ($y - $Radius) /2;
            $Inner_Circle = ( ( ($_x*$_x) + ($_y*$_y) ) < ($r*$r) );
            $top_Left = ($x > $Radius || $y > $Radius) || $Inner_Circle;

            $_x = ($x - $Radius) /2 - ($w/2 - $Radius);
            $_y = ($y - $Radius) /2;
            $Inner_Circle = ( ( ($_x*$_x) + ($_y*$_y) ) < ($r*$r) );
            $top_Right = ($x < ($w - $Radius) || $y > $Radius) || $Inner_Circle;

            $_x = ($x - $Radius) /2;
            $_y = ($y - $Radius) /2 - ($h/2 - $Radius);
            $Inner_Circle = ( ( ($_x*$_x) + ($_y*$_y) ) < ($r*$r) );
            $Bottom_Left =  ($x > $Radius || $y < ($h - $Radius) ) || $Inner_Circle;

            $_x = ($x - $Radius) /2 - ($w/2 - $Radius);
            $_y = ($y - $Radius) /2 - ($h/2 - $Radius);
            $Inner_Circle = ( ( ($_x*$_x) + ($_y*$_y) ) < ($r*$r) );
            $Bottom_Right = ($x < ($w - $Radius) || $y < ($h - $Radius) ) || $Inner_Circle;

            if($top_Left && $top_Right && $Bottom_Left && $Bottom_Right ){

                imagesetpixel($newpic,$x,$y,$c);

            }else{
                imagesetpixel($newpic,$x,$y,$transparent);
            }

        }
    }


    if( !$Keep_SourceFile && $source_url != $destination_url){
        unlink($source_url);
    }


    imagesavealpha($newpic, true);
    imagepng($newpic, $destination_url);
    imagedestroy($newpic);
    imagedestroy($src);


    return $destination_url;
}

Upvotes: 0

christian
christian

Reputation: 558

For those who wants a solution in PHP, providing the picture already cropped into a circle:

// convert the picture
    $w = 640;  $h=480; // original size
    $original_path="/location/of/your/original-picture.jpg";
    $dest_path="/location/of/your/picture-crop-transp.png";
    $src = imagecreatefromstring(file_get_contents($original_path));
    $newpic = imagecreatetruecolor($w,$h);
    imagealphablending($newpic,false);
    $transparent = imagecolorallocatealpha($newpic, 0, 0, 0, 127);
    $r=$w/2;
    for($x=0;$x<$w;$x++)
        for($y=0;$y<$h;$y++){
            $c = imagecolorat($src,$x,$y);
            $_x = $x - $w/2;
            $_y = $y - $h/2;
            if((($_x*$_x) + ($_y*$_y)) < ($r*$r)){
                imagesetpixel($newpic,$x,$y,$c);
            }else{
                imagesetpixel($newpic,$x,$y,$transparent);
            }
        }
    imagesavealpha($newpic, true);
    imagepng($newpic, $dest_path);
    imagedestroy($newpic);
    imagedestroy($src);

Upvotes: 2

parliament
parliament

Reputation: 22964

For those who want a node/js based solution, you can create a circular crop at specified coordinates using node-gm like this:

gm(original)
   .crop(233, 233,29,26)
   .resize(size, size)
   .write(output, function(err) {
      gm(size, size, 'none')
         .fill(output)
         .drawCircle(size/2,size/2, size/2, 0)
         .write(output, function(err) {
            console.log(err || 'done');
         });
    });

You can use a lib like JCrop (demo) to allow the user to crop the image on the front-end and pass the coordinates (w,h,x,y) into crop().

Upvotes: 0

tom
tom

Reputation: 2077

Here is one way with ImageMagick that will acomplish this without using a mask:

convert -size 200x200 xc:none -fill walter.jpg -draw "circle 100,100 100,1" circle_thumb.png

walter alt text

Upvotes: 52

Jonathan Hell
Jonathan Hell

Reputation: 529

For those who need to do this in pure PHP using Imagick, you will need to refer to this question : circularize an image with imagick

Hope this will help.

J.

Upvotes: 2

Related Questions