如何在UIImage的不透明像素周围绘制一个边框(笔触)。

问题描述 投票:0回答:1

给定一个包含非不透明(alpha < 1)像素数据的UIImage,我们如何在不透明(alpha > 0)的像素周围绘制一个轮廓边框描边,并自定义描边颜色和厚度?(我提出这个问题是为了在下面提供答案)

swift uiimage border core-graphics stroke
1个回答
1
投票

我通过拼凑其他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
    }

}

现在我们可以获得一个视图的图像,我们需要能够将它裁剪成不透明的像素。这一步是可选的,但对于像UILabels这样的东西来说,有时它们的边界比它们显示的像素要大。下面的函数需要一个完成块,这样它就可以在后台线程上执行繁重的工作(注意:UIKit不是线程安全的,但CGContexts是)。

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)
            }

        }

    }

}

最后,我们需要能够用我们选择的颜色填充一个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)
        }
    }

}

现在我们可以开始着手解决这个问题了,在我们渲染的裁剪过的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

    }

}

把所有的步骤结合在一起,你就可以像这样在一个UIImage上应用一个描边。

let inputImage: UIImage = UIImage()
let outputImage = inputImage.imageByApplyingStroke(strokeColor: .black, strokeThickness: 2.0)

下面是一个描边操作的例子,应用于一个带有白色文字的标签,并有一个黑色的描边。

Example of stroke code

© www.soinside.com 2019 - 2024. All rights reserved.