-
-
Save chriszielinski/aec9a2f2ba54745dc715dd55f5718177 to your computer and use it in GitHub Desktop.
// 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) }) | |
}) | |
} | |
} |
You are such a master. I can apply your function in real time and it works perfectly. THANK YOU
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)
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.
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?
fixed memory leak
https://gist.github.com/msnazarow/a4cc6e8a0a7f1e075a25039ec3e32aca
Several things could be improved to give a boost on up to 10 times faster performance:
- Use the fixed memory leak version: https://gist.github.com/msnazarow/a4cc6e8a0a7f1e075a25039ec3e32aca
- In
isPixelRowTransparent
, ifmaximumAlphaChannel == 0
, the second slow condition no need to check. - Get
topInset
andbottomOpaqueRow
in the separateguard
:- create a new range from them:
rowRange = topInset ..< bottomOpaqueRow
- send
rowRange
tofirstOpaquePixelColumn
and use it instead ofpixelRowRange
.
- create a new range from them:
- Also
assert(cgContext.data != nil)
could be made once at the beginning of thetrim()
function.
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.