Ada*_*eld 1 border core-graphics uiimage stroke swift
给定一个包含非不透明(alpha < 1)像素数据的 UIImage,我们如何使用自定义描边颜色和粗细在不透明(alpha > 0)像素周围绘制轮廓/边框/描边?(我问这个问题是为了在下面提供答案)
我通过将其他 SO 帖子的建议拼凑在一起并根据我满意的内容进行调整,想出了以下方法。
第一步是获取 UIImage 来开始处理。在某些情况下,您可能已经有了此图像,但如果您可能想要向 UIView 添加笔划(可能是具有自定义字体的 UILabel),您首先需要捕获该视图的图像:
public extension UIView {
/// Renders the view to a UIImage
/// - Returns: A UIImage representing the view
func imageByRenderingView() -> UIImage {
layoutIfNeeded()
let rendererFormat = UIGraphicsImageRendererFormat.default()
rendererFormat.scale = layer.contentsScale
rendererFormat.opaque = false
let renderer = UIGraphicsImageRenderer(size: bounds.size, format: rendererFormat)
let image = renderer.image { _ in
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
}
return image
}
}
Run Code Online (Sandbox Code Playgroud)
现在我们可以获得视图的图像,我们需要能够将其裁剪为不透明像素。此步骤是可选的,但对于 UILabels 之类的东西,有时它们的边界大于它们显示的像素。下面的函数采用一个完成块,因此它可以在后台线程上执行繁重的工作(注意:UIKit 不是线程安全的,但 CGContext 是)。
public extension UIImage {
/// Converts the image's color space to the specified color space
/// - Parameter colorSpace: The color space to convert to
/// - Returns: A CGImage in the specified color space
func cgImageInColorSpace(_ colorSpace: CGColorSpace) -> CGImage? {
guard let cgImage = self.cgImage else {
return nil
}
guard cgImage.colorSpace != colorSpace else {
return cgImage
}
let rect = CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height)
let ciImage = CIImage(cgImage: cgImage)
guard let convertedImage = ciImage.matchedFromWorkingSpace(to: CGColorSpaceCreateDeviceRGB()) else {
return nil
}
let ciContext = CIContext()
let convertedCGImage = ciContext.createCGImage(convertedImage, from: rect)
return convertedCGImage
}
/// Crops the image to the bounding box containing it's opaque pixels, trimming away fully transparent pixels
/// - Parameter minimumAlpha: The minimum alpha value to crop out of the image
/// - Parameter completion: A completion block to execute as the processing takes place on a background thread
func imageByCroppingToOpaquePixels(withMinimumAlpha minimumAlpha: CGFloat = 0, _ completion: @escaping ((_ image: UIImage)->())) {
guard let originalImage = cgImage else {
completion(self)
return
}
// Move to a background thread for the heavy lifting
DispatchQueue.global(qos: .background).async {
// Ensure we have the correct colorspace so we can safely iterate over the pixel data
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let cgImage = self.cgImageInColorSpace(colorSpace) else {
DispatchQueue.main.async {
completion(UIImage())
}
return
}
// Store some helper variables for iterating the pixel data
let width: Int = cgImage.width
let height: Int = cgImage.height
let bytesPerPixel: Int = cgImage.bitsPerPixel / 8
let bytesPerRow: Int = cgImage.bytesPerRow
let bitsPerComponent: Int = cgImage.bitsPerComponent
let bitmapInfo: UInt32 = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
// Attempt to access our pixel data
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 {
DispatchQueue.main.async {
completion(UIImage())
}
return
}
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
var minX: Int = width
var minY: Int = height
var maxX: Int = 0
var maxY: Int = 0
for x in 0 ..< width {
for y in 0 ..< height {
let pixelIndex = bytesPerRow * Int(y) + bytesPerPixel * Int(x)
let alphaAtPixel = CGFloat(ptr[pixelIndex + 3]) / 255.0
if alphaAtPixel > minimumAlpha {
if x < minX { minX = x }
if x > maxX { maxX = x }
if y < minY { minY = y }
if y > maxY { maxY = y }
}
}
}
let rectangleForOpaquePixels = CGRect(x: CGFloat(minX), y: CGFloat(minY), width: CGFloat( maxX - minX ), height: CGFloat( maxY - minY ))
guard let croppedImage = originalImage.cropping(to: rectangleForOpaquePixels) else {
DispatchQueue.main.async {
completion(UIImage())
}
return
}
DispatchQueue.main.async {
let result = UIImage(cgImage: croppedImage, scale: self.scale, orientation: self.imageOrientation)
completion(result)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
最后,我们需要能够用我们选择的颜色填充 UIImage:
public extension UIImage {
/// Returns a version of this image any non-transparent pixels filled with the specified color
/// - Parameter color: The color to fill
/// - Returns: A re-colored version of this image with the specified color
func imageByFillingWithColor(_ color: UIColor) -> UIImage {
return UIGraphicsImageRenderer(size: size).image { context in
color.setFill()
context.fill(context.format.bounds)
draw(in: context.format.bounds, blendMode: .destinationIn, alpha: 1.0)
}
}
}
Run Code Online (Sandbox Code Playgroud)
现在我们可以解决手头的问题,向渲染/裁剪的 UIImage 添加描边。此过程涉及将输入图像淹没为所需的笔划颜色,然后以圆形形式渲染图像,该图像从原始图像的中心点偏移笔划厚度。我们在 0...360 度范围内绘制此“笔划”图像的次数越多,生成的笔划就会显得越“精确”。话虽这么说,默认的 8 笔画似乎足以满足大多数情况(导致以 0、45、90、135、180、225 和 270 度间隔渲染笔画)。
此外,我们还需要针对给定角度多次绘制该笔画图像。在大多数情况下,每个角度绘制 1 次就足够了,但随着所需笔划粗细的增加,我们沿给定角度绘制笔划图像的次数也应该增加,以保持美观的笔划。
当所有笔画都绘制完毕后,我们在新图像的中心重新绘制原始图像,使其出现在所有绘制的笔画图像的前面。
下面的函数负责处理这些剩余的步骤:
public extension UIImage {
/// Applies a stroke around the image
/// - Parameters:
/// - strokeColor: The color of the desired stroke
/// - inputThickness: The thickness, in pixels, of the desired stroke
/// - rotationSteps: The number of rotations to make when applying the stroke. Higher rotationSteps will result in a more precise stroke. Defaults to 8.
/// - extrusionSteps: The number of extrusions to make along a given rotation. Higher extrusions will make a more precise stroke, but aren't usually needed unless using a very thick stroke. Defaults to 1.
func imageByApplyingStroke(strokeColor: UIColor = .white, strokeThickness inputThickness: CGFloat = 2, rotationSteps: Int = 8, extrusionSteps: Int = 1) -> UIImage {
let thickness: CGFloat = inputThickness > 0 ? inputThickness : 0
// Create a "stamp" version of ourselves that we can stamp around our edges
let strokeImage = imageByFillingWithColor(strokeColor)
let inputSize: CGSize = size
let outputSize: CGSize = CGSize(width: size.width + (thickness * 2), height: size.height + (thickness * 2))
let renderer = UIGraphicsImageRenderer(size: outputSize)
let stroked = renderer.image { ctx in
// Compute the center of our image
let center = CGPoint(x: outputSize.width / 2, y: outputSize.height / 2)
let centerRect = CGRect(x: center.x - (inputSize.width / 2), y: center.y - (inputSize.height / 2), width: inputSize.width, height: inputSize.height)
// Compute the increments for rotations / extrusions
let rotationIncrement: CGFloat = rotationSteps > 0 ? 360 / CGFloat(rotationSteps) : 360
let extrusionIncrement: CGFloat = extrusionSteps > 0 ? thickness / CGFloat(extrusionSteps) : thickness
for rotation in 0..<rotationSteps {
for extrusion in 1...extrusionSteps {
// Compute the angle and distance for this stamp
let angleInDegrees: CGFloat = CGFloat(rotation) * rotationIncrement
let angleInRadians: CGFloat = angleInDegrees * .pi / 180.0
let extrusionDistance: CGFloat = CGFloat(extrusion) * extrusionIncrement
// Compute the position for this stamp
let x = center.x + extrusionDistance * cos(angleInRadians)
let y = center.y + extrusionDistance * sin(angleInRadians)
let vector = CGPoint(x: x, y: y)
// Draw our stamp at this position
let drawRect = CGRect(x: vector.x - (inputSize.width / 2), y: vector.y - (inputSize.height / 2), width: inputSize.width, height: inputSize.height)
strokeImage.draw(in: drawRect, blendMode: .destinationOver, alpha: 1.0)
}
}
// Finally, re-draw ourselves centered within the context, so we appear in-front of all of the stamps we've drawn
self.draw(in: centerRect, blendMode: .normal, alpha: 1.0)
}
return stroked
}
}
Run Code Online (Sandbox Code Playgroud)
将它们组合在一起,您可以将笔划应用于 UIImage,如下所示:
let inputImage: UIImage = UIImage()
let outputImage = inputImage.imageByApplyingStroke(strokeColor: .black, strokeThickness: 2.0)
Run Code Online (Sandbox Code Playgroud)
以下是实际描边的示例,应用于带有白色文本和黑色描边的标签: