I have a UIImage which contains a shape; the rest is transparent. I'd like to get another UIImage by cropping out as much of the transparent part as possible, still retaining all of the non-transparent pixels - similar to the autocrop function in GIMP. How would I go about doing this?
6 Answers
This approach may be a little more invasive than what you were hoping for, but it gets the job done. What I'm doing is creating a bitmap context for the UIImage, obtaining a pointer to the raw image data, then sifting through it looking for non-transparent pixels. My method returns a CGRect which I use to create a new UIImage.
- (CGRect)cropRectForImage:(UIImage *)image {
CGImageRef cgImage = image.CGImage;
CGContextRef context = [self createARGBBitmapContextFromImage:cgImage];
if (context == NULL) return CGRectZero;
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
CGRect rect = CGRectMake(0, 0, width, height);
CGContextDrawImage(context, rect, cgImage);
unsigned char *data = CGBitmapContextGetData(context);
CGContextRelease(context);
//Filter through data and look for non-transparent pixels.
int lowX = width;
int lowY = height;
int highX = 0;
int highY = 0;
if (data != NULL) {
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
int pixelIndex = (width * y + x) * 4 /* 4 for A, R, G, B */;
if (data[pixelIndex] != 0) { //Alpha value is not zero; pixel is not transparent.
if (x < lowX) lowX = x;
if (x > highX) highX = x;
if (y < lowY) lowY = y;
if (y > highY) highY = y;
}
}
}
free(data);
} else {
return CGRectZero;
}
return CGRectMake(lowX, lowY, highX-lowX, highY-lowY);
}
The method to create the Bitmap Context:
- (CGContextRef)createARGBBitmapContextFromImage:(CGImageRef)inImage {
CGContextRef context = NULL;
CGColorSpaceRef colorSpace;
void *bitmapData;
int bitmapByteCount;
int bitmapBytesPerRow;
// Get image width, height. We'll use the entire image.
size_t width = CGImageGetWidth(inImage);
size_t height = CGImageGetHeight(inImage);
// Declare the number of bytes per row. Each pixel in the bitmap in this
// example is represented by 4 bytes; 8 bits each of red, green, blue, and
// alpha.
bitmapBytesPerRow = (width * 4);
bitmapByteCount = (bitmapBytesPerRow * height);
// Use the generic RGB color space.
colorSpace = CGColorSpaceCreateDeviceRGB();
if (colorSpace == NULL) return NULL;
// Allocate memory for image data. This is the destination in memory
// where any drawing to the bitmap context will be rendered.
bitmapData = malloc( bitmapByteCount );
if (bitmapData == NULL)
{
CGColorSpaceRelease(colorSpace);
return NULL;
}
// Create the bitmap context. We want pre-multiplied ARGB, 8-bits
// per component. Regardless of what the source image format is
// (CMYK, Grayscale, and so on) it will be converted over to the format
// specified here by CGBitmapContextCreate.
context = CGBitmapContextCreate (bitmapData,
width,
height,
8, // bits per component
bitmapBytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedFirst);
if (context == NULL) free (bitmapData);
// Make sure and release colorspace before returning
CGColorSpaceRelease(colorSpace);
return context;
}
And finally, get your new cropped UIImage from the returned CGRect:
CGRect newRect = [self cropRectForImage:oldImage];
CGImageRef imageRef = CGImageCreateWithImageInRect(oldImage.CGImage, newRect);
UIImage *newImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
I grabbed a bit of that code from this very useful article. Hope it helps!

- 6,876
- 1
- 33
- 42
-
Thanks. Working great... :) – Thampuran Nov 11 '14 at 19:21
-
Somehow processed image is clipped at the bottom. I don't have any clue why it's happening. The `newRect` has smaller height than expected. – dev gr Feb 14 '20 at 06:55
Swift Version:
extension UIImage {
func cropRect() -> CGRect {
let cgImage = self.CGImage!
let context = createARGBBitmapContextFromImage(cgImage)
if context == nil {
return CGRectZero
}
let height = CGFloat(CGImageGetHeight(cgImage))
let width = CGFloat(CGImageGetWidth(cgImage))
let rect = CGRectMake(0, 0, width, height)
CGContextDrawImage(context, rect, cgImage)
let data = UnsafePointer<CUnsignedChar>(CGBitmapContextGetData(context))
if data == nil {
return CGRectZero
}
var lowX = width
var lowY = height
var highX: CGFloat = 0
var highY: CGFloat = 0
//Filter through data and look for non-transparent pixels.
for (var y: CGFloat = 0 ; y < height ; y++) {
for (var x: CGFloat = 0; x < width ; x++) {
let pixelIndex = (width * y + x) * 4 /* 4 for A, R, G, B */
if data[Int(pixelIndex)] != 0 { //Alpha value is not zero pixel is not transparent.
if (x < lowX) {
lowX = x
}
if (x > highX) {
highX = x
}
if (y < lowY) {
lowY = y
}
if (y > highY) {
highY = y
}
}
}
}
return CGRectMake(lowX, lowY, highX-lowX, highY-lowY)
}
}
The method to create the Bitmap Context:
func createARGBBitmapContextFromImage(inImage: CGImageRef) -> CGContextRef? {
let width = CGImageGetWidth(inImage)
let height = CGImageGetHeight(inImage)
let bitmapBytesPerRow = width * 4
let bitmapByteCount = bitmapBytesPerRow * height
let colorSpace = CGColorSpaceCreateDeviceRGB()
if colorSpace == nil {
return nil
}
let bitmapData = malloc(bitmapByteCount)
if bitmapData == nil {
return nil
}
let context = CGBitmapContextCreate (bitmapData,
width,
height,
8, // bits per component
bitmapBytesPerRow,
colorSpace,
CGImageAlphaInfo.PremultipliedFirst.rawValue)
return context
}
And finally, get your new cropped UIImage from the returned CGRect:
let image = // UIImage Source
let newRect = image.cropRect()
if let imageRef = CGImageCreateWithImageInRect(image.CGImage!, newRect) {
let newImage = UIImage(CGImage: imageRef)
// Use this new Image
}

- 11,183
- 13
- 64
- 87
Swift 4
extension UIImage {
func cropAlpha() -> UIImage {
let cgImage = self.cgImage!;
let width = cgImage.width
let height = cgImage.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bytesPerPixel:Int = 4
let bytesPerRow = bytesPerPixel * width
let bitsPerComponent = 8
let bitmapInfo: UInt32 = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo),
let ptr = context.data?.assumingMemoryBound(to: UInt8.self) else {
return self
}
context.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: width, height: height))
var minX = width
var minY = height
var maxX: Int = 0
var maxY: Int = 0
for x in 1 ..< width {
for y in 1 ..< height {
let i = bytesPerRow * Int(y) + bytesPerPixel * Int(x)
let a = CGFloat(ptr[i + 3]) / 255.0
if(a>0) {
if (x < minX) { minX = x };
if (x > maxX) { maxX = x };
if (y < minY) { minY = y};
if (y > maxY) { maxY = y};
}
}
}
let rect = CGRect(x: CGFloat(minX),y: CGFloat(minY), width: CGFloat(maxX-minX), height: CGFloat(maxY-minY))
let imageScale:CGFloat = self.scale
let croppedImage = self.cgImage!.cropping(to: rect)!
let ret = UIImage(cgImage: croppedImage, scale: imageScale, orientation: self.imageOrientation)
return ret;
}
}

- 15,419
- 6
- 50
- 75

- 3,233
- 1
- 26
- 27
-
-
4I think there might be an off-by-one error in here. Specifically, I think that this cuts off the right column and bottom row of pixels by miscomputing the width and height. Since both the maxX and minX columns have pixels in them, the width should be CGFloat(maxX-minX+1), and likewise for the Y pixels and height. – Kyle Cronin Feb 05 '19 at 04:18
Improved from @Danny182's answer, i also added white-space (any pixels brighter than 0xe0e0e0) trimming for my own need.
Usage:
let newimage = UIImage(named: "XXX")!.trim()
import UIKit
extension UIImage {
func trim() -> UIImage {
let newRect = self.cropRect
if let imageRef = self.cgImage!.cropping(to: newRect) {
return UIImage(cgImage: imageRef)
}
return self
}
var cropRect: CGRect {
let cgImage = self.cgImage
let context = createARGBBitmapContextFromImage(inImage: cgImage!)
if context == nil {
return CGRect.zero
}
let height = CGFloat(cgImage!.height)
let width = CGFloat(cgImage!.width)
let rect = CGRect(x: 0, y: 0, width: width, height: height)
context?.draw(cgImage!, in: rect)
//let data = UnsafePointer<CUnsignedChar>(CGBitmapContextGetData(context))
guard let data = context?.data?.assumingMemoryBound(to: UInt8.self) else {
return CGRect.zero
}
var lowX = width
var lowY = height
var highX: CGFloat = 0
var highY: CGFloat = 0
let heightInt = Int(height)
let widthInt = Int(width)
//Filter through data and look for non-transparent pixels.
for y in (0 ..< heightInt) {
let y = CGFloat(y)
for x in (0 ..< widthInt) {
let x = CGFloat(x)
let pixelIndex = (width * y + x) * 4 /* 4 for A, R, G, B */
if data[Int(pixelIndex)] == 0 { continue } // crop transparent
if data[Int(pixelIndex+1)] > 0xE0 && data[Int(pixelIndex+2)] > 0xE0 && data[Int(pixelIndex+3)] > 0xE0 { continue } // crop white
if (x < lowX) {
lowX = x
}
if (x > highX) {
highX = x
}
if (y < lowY) {
lowY = y
}
if (y > highY) {
highY = y
}
}
}
return CGRect(x: lowX, y: lowY, width: highX - lowX, height: highY - lowY)
}
func createARGBBitmapContextFromImage(inImage: CGImage) -> CGContext? {
let width = inImage.width
let height = inImage.height
let bitmapBytesPerRow = width * 4
let bitmapByteCount = bitmapBytesPerRow * height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapData = malloc(bitmapByteCount)
if bitmapData == nil {
return nil
}
let context = CGContext (data: bitmapData,
width: width,
height: height,
bitsPerComponent: 8, // bits per component
bytesPerRow: bitmapBytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)
return context
}
}

- 951
- 1
- 8
- 9
-
3Works perfectly! One suggestion would be to apply Swift naming conventions and call the `trim()` function `trimmed()` since you return a new object. The naming would be correct if you return `void`and modify the existing UIImage instance. – blackjacx Jan 18 '17 at 09:26
-
2@Daddycat Tan Yin See, nice solution - how about memory management here? is malloc taken care of? – shadowf Jan 23 '18 at 23:52
-
For some reason, trim() is returning a bigger image than the original one, which is weird. I didn't manage to find the root issue, but @Jason Cragun worked fine to me – Juan Giorello Aug 18 '21 at 18:24
I present you here the version of Swift 4. In addition to the code above, everybody forget context.clear() !!
func cropRect() -> CGRect {
let cgImage = self.cgImage!
let bitmapBytesPerRow = cgImage.width * 4
let bitmapByteCount = bitmapBytesPerRow * cgImage.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapData = malloc(bitmapByteCount)
if bitmapData == nil {
return CGRect(x: 0, y: 0, width: 0, height: 0)
}
let context = CGContext (data: bitmapData, width: cgImage.width, height: cgImage.height, bitsPerComponent: 8,bytesPerRow: bitmapBytesPerRow,space: colorSpace,bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)
if context == nil {
return CGRect(x: 0, y: 0, width: 0, height: 0)
}
let height = cgImage.height
let width = cgImage.width
let rect = CGRect(x: 0, y: 0, width: width, height: height)
context?.clear(rect)
context?.draw(cgImage, in: rect)
let data = context?.data
if data == nil {
return CGRect(x: 0, y: 0, width: 0, height: 0)
}
var lowX = width
var lowY = height
var highX: Int = 0
var highY: Int = 0
//Filter through data and look for non-transparent pixels.
for y in 0..<height {
for x in 0..<width {
let pixelIndex = (width * y + x) * 4 /* 4 for A, R, G, B */
let color = data!.load(fromByteOffset: pixelIndex, as: UInt32.self)
if color != 0 { //Alpha value is not zero pixel is not transparent.
if (x < lowX) {
lowX = x
}
if (x > highX) {
highX = x
}
if (y < lowY) {
lowY = y
}
if (y > highY) {
highY = y
}
}
}
}
return CGRect(x: lowX, y: lowY, width: highX-lowX, height: highY-lowY)
}
For getting the cropped image, just usual:
UIImage(cgImage: image.cgImage!.cropping(to: croppedRect)!)

- 677
- 8
- 5
-
You forgot to call free(bitmapData) before leaving the method (either at end of method or during the two early returns). – Smartcat Jan 11 '23 at 19:30
Swift 3 (it's not a UIImage extension, I needed it in another class) it may save time for someone:
class EditImage {
static func cropRect(_ image: UIImage) -> CGRect {
let cgImage = image.cgImage
let context = createARGBBitmapContextFromImage(inImage: cgImage!)
if context == nil {
return CGRect.zero
}
let height = CGFloat(cgImage!.height)
let width = CGFloat(cgImage!.width)
let rect = CGRect(x: 0, y: 0, width: width, height: height)
context?.draw(cgImage!, in: rect)
//let data = UnsafePointer<CUnsignedChar>(CGBitmapContextGetData(context))
let data = context?.data?.assumingMemoryBound(to: UInt8.self)
if data == nil {
return CGRect.zero
}
var lowX = width
var lowY = height
var highX: CGFloat = 0
var highY: CGFloat = 0
let heightInt = Int(height)
let widthInt = Int(width)
//Filter through data and look for non-transparent pixels.
for y in (0 ..< heightInt) {
let y = CGFloat(y)
for x in (0 ..< widthInt) {
let x = CGFloat(x)
let pixelIndex = (width * y + x) * 4 /* 4 for A, R, G, B */
if data?[Int(pixelIndex)] != 0 { //Alpha value is not zero pixel is not transparent.
if (x < lowX) {
lowX = x
}
if (x > highX) {
highX = x
}
if (y < lowY) {
lowY = y
}
if (y > highY) {
highY = y
}
}
}
}
return CGRect(x: lowX, y: lowY, width: highX - lowY, height: highY - lowY)
}
static func createARGBBitmapContextFromImage(inImage: CGImage) -> CGContext? {
let width = inImage.width
let height = inImage.height
let bitmapBytesPerRow = width * 4
let bitmapByteCount = bitmapBytesPerRow * height
let colorSpace = CGColorSpaceCreateDeviceRGB()
if colorSpace == nil {
return nil
}
let bitmapData = malloc(bitmapByteCount)
if bitmapData == nil {
return nil
}
let context = CGContext (data: bitmapData,
width: width,
height: height,
bitsPerComponent: 8, // bits per component
bytesPerRow: bitmapBytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)
return context
}
}

- 2,708
- 1
- 25
- 37
-
1thanks for the code @Danny182, however there was a typo at the return line (highX - lowY) --> should be highX - lowX – Daddycat Tan Yin See Nov 24 '16 at 07:36
-
maybe that's why it wasn't working perfectly. I'll try it out. TY – DanielZanchi Nov 25 '16 at 17:11
-
CGColorSpaceCreateDeviceRGB() does not return an optional, the nil check it's not necessary. – Raul Huerta Sep 26 '17 at 19:51