Crop whitespace from image in PHP
Asked Answered
B

8

30

Is it possible to remove the whitespace surrounding an image in PHP?

NOTE: to clarify I mean something like photoshops trim feature.

Thanks.

Bloodstained answered 3/11, 2009 at 19:41 Comment(0)
E
57

To trim all whitespace, as you call it, surrounding the interesting part of the image, first we find out where the "whitespace" stops, and then we copy everything inside of those borders.

//load the image
$img = imagecreatefromjpeg("http://ecx.images-amazon.com/images/I/413XvF0yukL._SL500_AA280_.jpg");

//find the size of the borders
$b_top = 0;
$b_btm = 0;
$b_lft = 0;
$b_rt = 0;

//top
for(; $b_top < imagesy($img); ++$b_top) {
  for($x = 0; $x < imagesx($img); ++$x) {
    if(imagecolorat($img, $x, $b_top) != 0xFFFFFF) {
       break 2; //out of the 'top' loop
    }
  }
}

//bottom
for(; $b_btm < imagesy($img); ++$b_btm) {
  for($x = 0; $x < imagesx($img); ++$x) {
    if(imagecolorat($img, $x, imagesy($img) - $b_btm-1) != 0xFFFFFF) {
       break 2; //out of the 'bottom' loop
    }
  }
}

//left
for(; $b_lft < imagesx($img); ++$b_lft) {
  for($y = 0; $y < imagesy($img); ++$y) {
    if(imagecolorat($img, $b_lft, $y) != 0xFFFFFF) {
       break 2; //out of the 'left' loop
    }
  }
}

//right
for(; $b_rt < imagesx($img); ++$b_rt) {
  for($y = 0; $y < imagesy($img); ++$y) {
    if(imagecolorat($img, imagesx($img) - $b_rt-1, $y) != 0xFFFFFF) {
       break 2; //out of the 'right' loop
    }
  }
}

//copy the contents, excluding the border
$newimg = imagecreatetruecolor(
    imagesx($img)-($b_lft+$b_rt), imagesy($img)-($b_top+$b_btm));

imagecopy($newimg, $img, 0, 0, $b_lft, $b_top, imagesx($newimg), imagesy($newimg));

//finally, output the image
header("Content-Type: image/jpeg");
imagejpeg($newimg);

My old example, that assumes an identical "border" on all sides of the image, just to clarify the comments :)

//load the image
$img = imagecreatefromjpeg("img.jpg");

//find the size of the border.
$border = 0;
while(imagecolorat($img, $border, $border) == 0xFFFFFF) {
  $border++;
}

//copy the contents, excluding the border
//This code assumes that the border is the same size on all sides of the image.
$newimg = imagecreatetruecolor(imagesx($img)-($border*2), imagesy($img)-($border*2));
imagecopy($newimg, $img, 0, 0, $border, $border, imagesx($newimg), imagesy($newimg));

//finally, if you want, overwrite the original image
imagejpeg($newimg, "img.jpg");
Elviselvish answered 3/11, 2009 at 19:51 Comment(18)
Nice example... As you point out (just to clarify), this assumes a fixed-size border of white all around the image.Seignior
Hi, the border isn't a fixed size. I'm thinking of something like photoshops trim feature.Bloodstained
This code doesn't assume a fixed size of border (like, all borders are 14px), but it assumes that the border is the same size on all sides of the image. You can use this as a starting point, though. Remember that checking all pixels on all sides will get slow - don't do this every time you show the image, do it when the user uploads it the first time :)Elviselvish
@gnud: I see what your saying, you mean work out the white border on each side individually. Is the code above calculating the border from the top or left of the page?Bloodstained
Added a new example that might help - it's not tested at all, though. And I'm not sure how it deals with transparency. But you can perhaps figure that out on your own :)Elviselvish
All image coordinates in the GD library are 0,0 for top left. My original example (now the 2nd one) searches diagonally downwards towards the bottom right, looking for a non-white pixel.Elviselvish
Now it works perfectly for the top and left, but doesn't do anything for the bottom and right.Bloodstained
Like I said in my comment, I never tested any of this code. Start by fixing typos, and see where that gets you - if you provide an example image, I'll test it.Elviselvish
Thanks gnud, how about this one? ecx.images-amazon.com/images/I/413XvF0yukL._SL500_AA280_.jpgBloodstained
OK, I've made two changes, and it works for me. Fixed one typo, and added a "-1" to the bottom and right indexes, because the index starts at 0, not 1.Elviselvish
Excellent thanks, very good code. And thanks again for all the help.Bloodstained
Thanks! But what do I need to change to crop transparent borders?Metacarpus
This is what I was looking for.Turkey
@JohnSmith Slow by taking into mind more code written or in practical execution?Turkey
@Elviselvish Can you also give an example when the crop whitespace image may not bigger then 200px?Pasha
This answer appears to be out of date, seems like there are some built-in solutions given in the answers below.Exosmosis
@Elviselvish can I do this: $img = imagecreatefromstring($data) where $data is image string and then pass $img to your code instead of $img = imagecreatefromjpeg('url-here')? I tried that and it didn't work I just get purely black image! Not sure what to try.Clement
On JPEG-compressed images I had more success with // check if pixel is ~99% white $rgb = imagecolorat($img, $x, $b_top);$r = ($rgb >> 16) & 0xFF;$g = ($rgb >> 8) & 0xFF;$b = $rgb & 0xFF;$is99PercentWhite = $r > 250 && $g > 250 && $b > 250;if (!$is99PercentWhite) {break 2; //out of the 'top' loop } - JPEG compression artifacts would make it break prematurely on a 0xFFFFFF checkCalcar
G
13

Gnud's script redundantly calls imagesx and imagesy. It also iterates every pixel on every side, even when the corners overlap. This improved version eliminates redundant function calls and checks every pixel only once, granting a significant increase in speed. The function returns a status ($result['#']) equal to 2 if every pixel is the trimmed.

example();
function example(){
    $img = imagecreatefromjpeg("http://ecx.images-amazon.com/images/I/413XvF0yukL._SL500_AA280_.jpg");

    // find the trimmed image border
    $box = imageTrimBox($img);

    // copy cropped portion
    $img2 = imagecreate($box['w'], $box['h']);
    imagecopy($img2, $img, 0, 0, $box['l'], $box['t'], $box['w'], $box['h']);

    // output cropped image to the browser
    header('Content-Type: image/png');
    imagepng($img2);

    imagedestroy($img);
    imagedestroy($img2);
}



function imageTrimBox($img, $hex=null){
if (!ctype_xdigit($hex)) $hex = imagecolorat($img, 0,0);
$b_top = $b_lft = 0;
$b_rt = $w1 = $w2 = imagesx($img);
$b_btm = $h1 = $h2 = imagesy($img);

do {
    //top
    for(; $b_top < $h1; ++$b_top) {
        for($x = 0; $x < $w1; ++$x) {
            if(imagecolorat($img, $x, $b_top) != $hex) {
                break 2;
            }
        }
    }

    // stop if all pixels are trimmed
    if ($b_top == $b_btm) {
        $b_top = 0;
        $code = 2;
        break 1;
    }

    // bottom
    for(; $b_btm >= 0; --$b_btm) {
        for($x = 0; $x < $w1; ++$x) {
            if(imagecolorat($img, $x, $b_btm-1) != $hex) {
                break 2;
            }
        }
    }

    // left
    for(; $b_lft < $w1; ++$b_lft) {
        for($y = $b_top; $y <= $b_btm; ++$y) {
            if(imagecolorat($img, $b_lft, $y) != $hex) {
                break 2;
            }
        }
    }

    // right
    for(; $b_rt >= 0; --$b_rt) {
        for($y = $b_top; $y <= $b_btm; ++$y) {
            if(imagecolorat($img, $b_rt-1, $y) != $hex) {
                break 2;
            }
        }

    }

    $w2 = $b_rt - $b_lft;
    $h2 = $b_btm - $b_top;
    $code = ($w2 < $w1 || $h2 < $h1) ? 1 : 0;
} while (0);

// result codes:
// 0 = Trim Zero Pixels
// 1 = Trim Some Pixels
// 2 = Trim All Pixels
return array(
    '#'     => $code,   // result code
    'l'     => $b_lft,  // left
    't'     => $b_top,  // top
    'r'     => $b_rt,   // right
    'b'     => $b_btm,  // bottom
    'w'     => $w2,     // new width
    'h'     => $h2,     // new height
    'w1'    => $w1,     // original width
    'h1'    => $h1,     // original height
);
}
Gaulin answered 23/8, 2012 at 13:11 Comment(3)
Thanks for this. I ran into some issues with it trying to access pixels outside the range. Like if the image was 1500 pixels wide, then it would try to call imagecolorat($img, 1500, 5); $b_rt & $b_btm should be 1 less than the width & height, respectively.Ingrid
The above script works great but removes color from some images. Wondering why that happens.Funkhouser
@SunilChandurkar The function takes an optional color parameter, "$hex". If specified, that's what will be cropped. You can try 0xFFFFFF to crop pure white. If you do not specify a color, it will crop the image using the color of the top left pixel in the image. If the image is divided into two colors with the top half being solid red and the bottom half being solid blue, the red half will be cropped off. A less aggressive approach would require checking all four corners and only cropping when they are identical. A more aggress approach would crop multiple times, once for each corner color.Gaulin
C
10

PHP's gd library has the imagecropauto function (PHP version 5.5+):

<?php 
$img=imagecreatefrompng("tux.png"); // Load and instantiate the image
if($img) {
  $cropped=imagecropauto($img,IMG_CROP_DEFAULT); // Auto-crop the image

  imagedestroy($img); // Clean up as $img is no longer needed

  header("Content-type: image/png"); // Set the appropriate header so the browser
                                     // knows how to present it
  imagepng($cropped); // Return the newly cropped image
}

By default imagecropauto will try to crop using transparency, and then fall back on using the 4 corners of the image to attempt to detect the background to crop; I have also had success with the following constants in place of IMG_CROP_AUTO in the example above:

  • IMG_CROP_BLACK - Useful for images with a black background
  • IMG_CROP_WHITE - Useful for images with a white background
  • IMG_CROP_THRESHOLD - Allows you to set a colour and threshold to use when cropping
Calculous answered 14/11, 2017 at 23:58 Comment(1)
Good way of trimming white border without using 3rd party extensions like ImageMagicFlame
P
9

I know this is pretty old but if you have ImageMagick enabled you can use this method

Trim Image

Phillisphilly answered 26/8, 2010 at 22:20 Comment(3)
Imagick::trimImage now has a fuzz paramter: "By default target must match a particular pixel color exactly. However, in many cases two colors may differ by a small amount. The fuzz member of image defines how much tolerance is acceptable to consider two colors as the same. This parameter represents the variation on the quantum range."Traylor
I want to strip white border from images because it looks ugly on non-white backgrounds (red, black etc). Php's Imagick library only has the function Imagick-> trimImage() which only strips borders that are the same as background - useless for me.Breeching
Try using Imagick::shave(). As long as the borders are all equal, that should be able to help.Phillisphilly
D
2

I realize this is quite old but I have a slightly different take on trimming an image via GD. Instead of doing just one side at a time - do all four. It is faster and less expensive cpu-wise in some ways. However, if you stop the FOR loops the moment you find the top-bottom-left-right sides - that is faster than this.

So first there is:

#
#   Do all four sides at once
#
        echo "Finding the top-left-bottom-right edges of the image...please wait.\n";
        $top = 99999;
        $bot = -99999;
        $left = 99999;
        $right = -99999;
        for( $x=$offset; $x<($w-$offset); $x++ ){
            for( $y=$offset; $y<($h-$offset); $y++ ){
                $rgb = imagecolorat( $gd, $x, $y );
                if( $color != $rgb ){
                    $left = ($x < $left) ? $x : $left;
                    $right = ($x > $right) ? $x : $right;
                    $top = ($y < $top) ? $y : $top;
                    $bot = ($y > $bot) ? $y : $bot;
                    }
                }
            }

and then there is:

#
#   Top
#
            echo "Finding the top of the image\n";
            $top = null;
            for( $y=$offset; $y<($h-$offset); $y++ ){
                for( $x=$offset; $x<($w-$offset); $x++ ){
                    $rgb = imagecolorat( $gd, $x, $y );
                    if( $color != $rgb ){ $top = $y; break; }
                    }

                if( !is_null($top) ){ break; }
                }
#
#   Bottom
#
            echo "Finding the bottom of the image\n";
            $bot = null;
            for( $y=($h-$offset); $y>$offset; $y-- ){
                for( $x=$offset; $x<($w-$offset); $x++ ){
                    $rgb = imagecolorat( $gd, $x, $y );
                    if( $color != $rgb ){ $bot = $y; break; }
                    }

                if( !is_null($bot) ){ break; }
                }
#
#   Left
#
            echo "Finding the left of the image\n";
            $left = null;
            for( $x=$offset; $x<($w-$offset); $x++ ){
                for( $y=$offset; $y<($h-$offset); $y++ ){
                    $rgb = imagecolorat( $gd, $x, $y );
                    if( $color != $rgb ){ $left = $x; break; }
                    }

                if( !is_null($left) ){ break; }
                }
#
#   right
#
            echo "Finding the right of the image\n";
            $right = null;
            for( $x=($w-$offset); $x>$offset; $x-- ){
                for( $y=$offset; $y<($h-$offset); $y++ ){
                    $rgb = imagecolorat( $gd, $x, $y );
                    if( $color != $rgb ){ $right = $x; break; }
                    }

                if( !is_null($right) ){ break; }
                }

In both cases, the $color variable contains the first color dot in the image:

$color = imagecolorat( $gd, 0, 0 );

This is because in GIF images - the first dot is 99% of the time the transparent (or background) color. Also, the $offset is (for me) a way to say I know the image is only going to be so wide and so high. So if I draw something that is only a maximum of 256 by 256 but I put it on a 1024 x 1024 background I can whack off some of that background and make an offset of 255 thus making the FOR loops only go from 255 to (1024-255) or 769.

Ok - before someone asks - WHY I would do such a thing - Because some fonts (like Bastarda) don't have the correct font information in them and a 256pt output of the letter "z" produces an image where the bottom of the "z" goes past 256 (down to something like 512) so in order to get the entire image you have to start (or end) farther down than what you'd think the font would go. So I split the difference and whack off 255 pixels from either end. This was after actually seeing that Bastarda does this.

Some additional notes:

1. PNG images you CAN set up to be like GIF images but normally you will have to specify what the background color is going to be.
2. JPEG images do NOT uncompress the exact same way each time. So even comparing the same image you loaded twice might not work the same and may give different sizes.
3. These routines work best on simple black and white (or two color) images. Multiple colors can throw these routines off. Especially if you decide to use tolerances.
4. To use tolerances to determine if you have found the edge of an image, all you have to do is to pre-compute both the high and low tolerance (ie: if you have a tolerance of five(5) on the red component, then you can compute the tolerance as EITHER X-5-to-x+5 OR x-2.5-to-x+2.5 depending upon if you want the tolerance to be the WHOLE range or just the +/- range). You can have a tolerance for the RED, GREEN, BLUE, and ALPHA parts of the color or the entire color itself. So there are several different tolerances you can compute if you want and all of them are the correct way to do it depending upon your needs.

Donnell answered 29/12, 2015 at 7:23 Comment(0)
S
1

Check out the ImageMagick library in PHP. It has good methods of working with and manipulating images (including crop).

You'll have to figure out where the "whitespace" is around the image. It could be challenging, since "whitespace" could be the color white, some other color, transparency, etc...

Seignior answered 3/11, 2009 at 19:50 Comment(1)
I want to strip white border from images because it looks ugly on non-white backgrounds (red, black etc). Php's Imagick library only has the function Imagick-> trimImage() which only strips borders that are the same as background - useless for me.Breeching
O
1

Lets say you have image with border color 0x212121. You want to automatically trim this border with PHP. You can do it with such code:

// Load image
$img1='input.png';
$finfo = getimagesize($img1);
$image_old = $finfo['mime']=='image/png'?imagecreatefrompng($img1):imagecreatefromjpeg($img1);
// !! REMOVE BORDER !!
$cropped = imagecropauto($image_old , IMG_CROP_THRESHOLD, 1, 0x212121);
// Save image
imagepng($cropped, 'output.png');
Okoka answered 17/1, 2022 at 16:50 Comment(0)
F
0

I use strategy of gnud, but rewrite logic because in my case it doesn't work. My version:

class CutBackgroundGdImage
{

    protected $strip_colors = array(0xFFFFFF);

    public function setStripColors(array $strip_colors) {
      $this->strip_colors = $strip_colors;
      return $this;
    }

    /**
     * @param GdImage $image
     * @return GdImage
     */
    public function cutImage($image) {
      $top_edge = $this->findTopEdge($image);
      $bottom_edge = $this->findBottomEdge($image);
      $right_edge = $this->findRightEdge($image);
      $left_edge = $this->findLeftEdge($image);

      $cutted_image = imagecreatetruecolor(
        (imagesx($image) - $left_edge) - (imagesx($image) - $right_edge),
        (imagesy($image) - $top_edge) - (imagesy($image) - $bottom_edge)
      );

      imagecopy($cutted_image, $image, 0, 0, $left_edge, $top_edge, imagesx($cutted_image), imagesy($cutted_image));

      return $cutted_image;
    }

    public function findTopEdge($image) {
      for ($y = 0; $y < imagesy($image); $y++) {
        for ($x = 0; $x < imagesx($image); $x++) {
          if (!in_array(imagecolorat($image, $x, $y), $this->strip_colors)) {
            break 2;
          }
        }
      }
      return $y;
    }

    public function findBottomEdge($image) {
      for ($y = imagesy($image) - 1; $y > 0; $y--) {
        for ($x = 0; $x < imagesx($image); $x++) {
          if (!in_array(imagecolorat($image, $x, $y), $this->strip_colors)) {
            break 2;
          }
        }
      }
      return $y;
    }

    public function findLeftEdge($image) {
      for ($x = 0; $x < imagesx($image); $x++) {
        for($y = 0; $y < imagesy($image); $y++) {
          if (!in_array(imagecolorat($image, $x, $y), $this->strip_colors)) {
            break 2;
          }
        }
      }
      return $x;
    }

    public function findRightEdge($image) {
      for ($x = imagesx($image) - 1; $x > 0; $x--) {
        for ($y = 0; $y < imagesy($image); $y++) {
          if (!in_array(imagecolorat($image, $x, $y), $this->strip_colors)) {
            break 2;
          }
        }
      }
      return $x;
    }

}

$cut_gd_image = new CutBackgroundGdImage();
$cutted_image = $cut_gd_image->cutImage($gd_image);
Fishbein answered 2/7, 2022 at 7:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.