-
-
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
topInsetandbottomOpaqueRowin the separateguard:- create a new range from them:
rowRange = topInset ..< bottomOpaqueRow - send
rowRangetofirstOpaquePixelColumnand 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.