Skip to content

Instantly share code, notes, and snippets.

@chriszielinski
Last active December 26, 2023 03:27
Show Gist options
  • Star 35 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save chriszielinski/aec9a2f2ba54745dc715dd55f5718177 to your computer and use it in GitHub Desktop.
Save chriszielinski/aec9a2f2ba54745dc715dd55f5718177 to your computer and use it in GitHub Desktop.
[Swift 5] NSImage/UIImage Crop/Trim Transparency
// Image+Trim.swift
//
// Copyright © 2020 Christopher Zielinski.
// https://gist.github.com/chriszielinski/aec9a2f2ba54745dc715dd55f5718177
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if canImport(UIKit)
import UIKit
#else
import AppKit
#endif
#if canImport(UIKit)
typealias Image = UIImage
#else
typealias Image = NSImage
#endif
extension Image {
/// Crops the insets of transparency around the image.
///
/// - Parameters:
/// - maximumAlphaChannel: The maximum alpha channel value to consider _transparent_ and thus crop. Any alpha value
/// strictly greater than `maximumAlphaChannel` will be considered opaque.
func trimmingTransparentPixels(maximumAlphaChannel: UInt8 = 0) -> Image? {
guard size.height > 1 && size.width > 1
else { return self }
#if canImport(UIKit)
guard let cgImage = cgImage?.trimmingTransparentPixels(maximumAlphaChannel: maximumAlphaChannel)
else { return nil }
return UIImage(cgImage: cgImage, scale: scale, orientation: imageOrientation)
#else
guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil)?
.trimmingTransparentPixels(maximumAlphaChannel: maximumAlphaChannel)
else { return nil }
let scale = recommendedLayerContentsScale(0)
let scaledSize = CGSize(width: CGFloat(cgImage.width) / scale,
height: CGFloat(cgImage.height) / scale)
let image = NSImage(cgImage: cgImage, size: scaledSize)
image.isTemplate = isTemplate
return image
#endif
}
}
extension CGImage {
/// Crops the insets of transparency around the image.
///
/// - Parameters:
/// - maximumAlphaChannel: The maximum alpha channel value to consider _transparent_ and thus crop. Any alpha value
/// strictly greater than `maximumAlphaChannel` will be considered opaque.
func trimmingTransparentPixels(maximumAlphaChannel: UInt8 = 0) -> CGImage? {
return _CGImageTransparencyTrimmer(image: self, maximumAlphaChannel: maximumAlphaChannel)?.trim()
}
}
private struct _CGImageTransparencyTrimmer {
let image: CGImage
let maximumAlphaChannel: UInt8
let cgContext: CGContext
let zeroByteBlock: UnsafeMutableRawPointer
let pixelRowRange: Range<Int>
let pixelColumnRange: Range<Int>
init?(image: CGImage, maximumAlphaChannel: UInt8) {
guard let cgContext = CGContext(data: nil,
width: image.width,
height: image.height,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceGray(),
bitmapInfo: CGImageAlphaInfo.alphaOnly.rawValue),
cgContext.data != nil
else { return nil }
cgContext.draw(image,
in: CGRect(origin: .zero,
size: CGSize(width: image.width,
height: image.height)))
guard let zeroByteBlock = calloc(image.width, MemoryLayout<UInt8>.size)
else { return nil }
self.image = image
self.maximumAlphaChannel = maximumAlphaChannel
self.cgContext = cgContext
self.zeroByteBlock = zeroByteBlock
pixelRowRange = 0..<image.height
pixelColumnRange = 0..<image.width
}
func trim() -> CGImage? {
guard let topInset = firstOpaquePixelRow(in: pixelRowRange),
let bottomOpaqueRow = firstOpaquePixelRow(in: pixelRowRange.reversed()),
let leftInset = firstOpaquePixelColumn(in: pixelColumnRange),
let rightOpaqueColumn = firstOpaquePixelColumn(in: pixelColumnRange.reversed())
else { return nil }
let bottomInset = (image.height - 1) - bottomOpaqueRow
let rightInset = (image.width - 1) - rightOpaqueColumn
guard !(topInset == 0 && bottomInset == 0 && leftInset == 0 && rightInset == 0)
else { return image }
return image.cropping(to: CGRect(origin: CGPoint(x: leftInset, y: topInset),
size: CGSize(width: image.width - (leftInset + rightInset),
height: image.height - (topInset + bottomInset))))
}
@inlinable
func isPixelOpaque(column: Int, row: Int) -> Bool {
// Sanity check: It is safe to get the data pointer in iOS 4.0+ and macOS 10.6+ only.
assert(cgContext.data != nil)
return cgContext.data!.load(fromByteOffset: (row * cgContext.bytesPerRow) + column, as: UInt8.self)
> maximumAlphaChannel
}
@inlinable
func isPixelRowTransparent(_ row: Int) -> Bool {
assert(cgContext.data != nil)
// `memcmp` will efficiently check if the entire pixel row has zero alpha values
return memcmp(cgContext.data! + (row * cgContext.bytesPerRow), zeroByteBlock, image.width) == 0
// When the entire row is NOT zeroed, we proceed to check each pixel's alpha
// value individually until we locate the first "opaque" pixel (very ~not~ efficient).
|| !pixelColumnRange.contains(where: { isPixelOpaque(column: $0, row: row) })
}
@inlinable
func firstOpaquePixelRow<T: Sequence>(in rowRange: T) -> Int? where T.Element == Int {
return rowRange.first(where: { !isPixelRowTransparent($0) })
}
@inlinable
func firstOpaquePixelColumn<T: Sequence>(in columnRange: T) -> Int? where T.Element == Int {
return columnRange.first(where: { column in
pixelRowRange.contains(where: { isPixelOpaque(column: column, row: $0) })
})
}
}
@albbadia
Copy link

albbadia commented Apr 17, 2020

Works PERFECTLY. I have been using another library that was very very slow, so I used that only to save thumbnails although sometimes I had problems with transparent pixels with bigger images. With this code the process is very very fast and I can apply to all the images. And futhermore, for Appkit. Claps.

@rodrigoalvarez
Copy link

You are such a master. I can apply your function in real time and it works perfectly. THANK YOU

@KOSURUUDAYSAIKUMAR
Copy link

KOSURUUDAYSAIKUMAR commented Aug 9, 2021

Hi, Can anyone explain me.
I have an requirement.
I wrote the code but it is not working can you please help me.
cell.logoImageView.image = cell.logoImageView.image?.trimmingTransparentPixels(maximumAlphaChannel: 0)

@chriszielinski
Copy link
Author

Hi, Can anyone explain me.
I have an requirement.
I wrote the code but it is not working can you please help me.
cell.logoImageView.image = cell.logoImageView.image?.trimmingTransparentPixels(maximumAlphaChannel: 0)

You probably have pixels that are partially transparent. Carefully increase the maximumAlphaChannel until it trims those semi-opaque pixels.

@Megatron1000
Copy link

This works great and is very fast. One thing I don't understand though. What is the maximumAlphaChannel range? Obviously 0 is completely transparent but what counts as fully opaque? I've experimented with a radial gradient with an alpha going from transparent to opaque and increasing the maximumAlphaChannel definitely does have an affect but even if I set maximumAlphaChannel to 200 it's still not getting to the centre of my gradient so I'm not sure what this calculation is based on or how I work out the max?

@msnazarow
Copy link

@buh
Copy link

buh commented Jul 6, 2023

Several things could be improved to give a boost on up to 10 times faster performance:

  1. Use the fixed memory leak version: https://gist.github.com/msnazarow/a4cc6e8a0a7f1e075a25039ec3e32aca
  2. In isPixelRowTransparent, if maximumAlphaChannel == 0, the second slow condition no need to check.
  3. Get topInset and bottomOpaqueRow in the separate guard:
    • create a new range from them: rowRange = topInset ..< bottomOpaqueRow
    • send rowRange to firstOpaquePixelColumn and use it instead of pixelRowRange.
  4. Also assert(cgContext.data != nil) could be made once at the beginning of the trim() function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment