如何在Swift中为UIImage添加彩色边框

15

使用图层(borderWidth、borderColor等)很容易向UIImageView添加边框。是否有可能向图像添加边框,而不是向图像视图添加边框?有人知道吗?

更新:

我尝试遵循下面的建议并使用扩展(extension)。谢谢,但我没有得到所需的结果。这是我的代码。哪里出了问题?

import UIKit
    class ViewController: UIViewController {

    var imageView: UIImageView!
    var sizeW = CGFloat()
    var sizeH = CGFloat()

    override func viewDidLoad() {
        super.viewDidLoad()

        sizeW = view.frame.width
        sizeH = view.frame.height

        setImage()
    }

    func setImage(){

        //add image view
        imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: sizeW/2, height: sizeH/2))
        imageView.center = view.center
        imageView.tintColor = UIColor.orange
        imageView.contentMode = UIViewContentMode.scaleAspectFit

        let imgOriginal = UIImage(named: "plum")!.withRenderingMode(.alwaysTemplate)
        let borderImage = imgOriginal.imageWithBorder(width: 2, color: UIColor.blue)
        imageView.image = borderImage

        view.addSubview(imageView)

    }



}


    extension UIImage {
    func imageWithBorder(width: CGFloat, color: UIColor) -> UIImage? {
        let square = CGSize(width: min(size.width, size.height) + width * 2, height: min(size.width, size.height) + width * 2)
        let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square))
        imageView.contentMode = .center
        imageView.image = self
        imageView.layer.borderWidth = width
        imageView.layer.borderColor = color.cgColor
        UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        imageView.layer.render(in: context)
        let result = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return result
    }
}

红边框的第二张图片大致符合我的需求:

输入图片描述

输入图片描述


你在这里需要什么? - Dharmesh Kheni
我需要在李子本身周围,而不是图像视图周围,加上borderWidth为3的红色边框。我该怎么做? - Roman
我添加了第二张图片,展示了更或者说是不太理想的结果。 - Roman
有什么解决方案吗,@roman? - Yagiz
4个回答

23

受@herme5的强烈启发,进行了重构,更紧凑的Swift 5/iOS12+代码如下(还解决了垂直翻转问题):

public extension UIImage {

    /**
    Returns the flat colorized version of the image, or self when something was wrong

    - Parameters:
        - color: The colors to user. By defaut, uses the ``UIColor.white`

    - Returns: the flat colorized version of the image, or the self if something was wrong
    */
    func colorized(with color: UIColor = .white) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(size, false, scale)

        defer {
            UIGraphicsEndImageContext()
        }

        guard let context = UIGraphicsGetCurrentContext(), let cgImage = cgImage else { return self }


        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)

        color.setFill()
        context.translateBy(x: 0, y: size.height)
        context.scaleBy(x: 1.0, y: -1.0)
        context.clip(to: rect, mask: cgImage)
        context.fill(rect)

        guard let colored = UIGraphicsGetImageFromCurrentImageContext() else { return self }

        return colored
    }

    /**
    Returns the stroked version of the fransparent image with the given stroke color and the thickness.

    - Parameters:
        - color: The colors to user. By defaut, uses the ``UIColor.white`
        - thickness: the thickness of the border. Default to `2`
        - quality: The number of degrees (out of 360): the smaller the best, but the slower. Defaults to `10`.

    - Returns: the stroked version of the image, or self if something was wrong
    */

    func stroked(with color: UIColor = .white, thickness: CGFloat = 2, quality: CGFloat = 10) -> UIImage {

        guard let cgImage = cgImage else { return self }

        // Colorize the stroke image to reflect border color
        let strokeImage = colorized(with: color)

        guard let strokeCGImage = strokeImage.cgImage else { return self }

        /// Rendering quality of the stroke
        let step = quality == 0 ? 10 : abs(quality)

        let oldRect = CGRect(x: thickness, y: thickness, width: size.width, height: size.height).integral
        let newSize = CGSize(width: size.width + 2 * thickness, height: size.height + 2 * thickness)
        let translationVector = CGPoint(x: thickness, y: 0)


        UIGraphicsBeginImageContextWithOptions(newSize, false, scale)

        guard let context = UIGraphicsGetCurrentContext() else { return self }

        defer {
            UIGraphicsEndImageContext()
        }
        context.translateBy(x: 0, y: newSize.height)
        context.scaleBy(x: 1.0, y: -1.0)
        context.interpolationQuality = .high

        for angle: CGFloat in stride(from: 0, to: 360, by: step) {
            let vector = translationVector.rotated(around: .zero, byDegrees: angle)
            let transform = CGAffineTransform(translationX: vector.x, y: vector.y)

            context.concatenate(transform)

            context.draw(strokeCGImage, in: oldRect)

            let resetTransform = CGAffineTransform(translationX: -vector.x, y: -vector.y)
            context.concatenate(resetTransform)
        }

        context.draw(cgImage, in: oldRect)

        guard let stroked = UIGraphicsGetImageFromCurrentImageContext() else { return self }

        return stroked
    }
}


extension CGPoint {
    /** 
    Rotates the point from the center `origin` by `byDegrees` degrees along the Z axis.

    - Parameters:
        - origin: The center of he rotation;
        - byDegrees: Amount of degrees to rotate around the Z axis.

    - Returns: The rotated point.
    */
    func rotated(around origin: CGPoint, byDegrees: CGFloat) -> CGPoint {
        let dx = x - origin.x
        let dy = y - origin.y
        let radius = sqrt(dx * dx + dy * dy)
        let azimuth = atan2(dy, dx) // in radians
        let newAzimuth = azimuth + byDegrees * .pi / 180.0 // to radians
        let x = origin.x + radius * cos(newAzimuth)
        let y = origin.y + radius * sin(newAzimuth)
        return CGPoint(x: x, y: y)
    }
}

Result obtained for image.stroked()


1
你忘记包含 CGPoint.rotated 函数了。 - David
1
当然。我想尝试一下你的代码来勾勒国旗,但是顶部的轮廓线消失了。基本上,在调用stroked之后,UIImage在底部和两侧都有轮廓线。你知道为什么会发生这种情况吗?我正在使用一个普通的UIImageView,其中包含从维基百科获取的国旗URL。 - David
1
@StéphanedeLuca,我没有注意到我的编辑不需要同行审批。顺便说一下,你在上次编辑中更改了错误的“size”对象,我已经重新编辑了它,你可以查看编辑历史记录,现在应该一切都好了。敬礼。 - Husam
1
太棒了!我搜了很多,你的方法是最好的解决方案! - oOEric
1
谢谢,这对我的图标完美地起作用了。 - Esko918
显示剩余10条评论

13

这是我用Swift 4写的UIImage扩展。像IOSDealBreaker说的,这全都与图像处理有关,一些特定情况可能会发生。您应该有一个具有透明背景的png图像,并在大于原始尺寸时管理其大小。

  • 首先获取彩色阴影版本的图像。
  • 然后在给定的原点周围不断绘制阴影图像(在我们的情况下,在距离边界厚度处围绕 (0,0) 绘制)
  • 在原点处绘制源图像,以使其显示在前景中。
  • 如果边框超出原始矩形,则可能需要放大图像。

我的方法使用了许多实用程序方法和类扩展。这里有一些数学知识,可以围绕另一个点旋转向量(实际上是点):Rotating a CGPoint around another CGPoint

extension CGPoint {
    func rotated(around origin: CGPoint, byDegrees: CGFloat) -> CGPoint {
        let dx = self.x - origin.x
        let dy = self.y - origin.y
        let radius = sqrt(dx * dx + dy * dy)
        let azimuth = atan2(dy, dx) // in radians
        let newAzimuth = azimuth + (byDegrees * CGFloat.pi / 180.0) // convert it to radians
        let x = origin.x + radius * cos(newAzimuth)
        let y = origin.y + radius * sin(newAzimuth)
        return CGPoint(x: x, y: y)
    }
}

我编写了自定义的CIFilter来给具有透明背景的图像上色:在Swift中为UIImage上色

class ColorFilter: CIFilter {
    var inputImage: CIImage?
    var inputColor: CIColor?
    private let kernel: CIColorKernel = {
        let kernelString =
        """
        kernel vec4 colorize(__sample pixel, vec4 color) {
            pixel.rgb = pixel.a * color.rgb;
            pixel.a *= color.a;
            return pixel;
        }
        """
        return CIColorKernel(source: kernelString)!
    }()

    override var outputImage: CIImage? {
        guard let inputImage = inputImage, let inputColor = inputColor else { return nil }
        let inputs = [inputImage, inputColor] as [Any]
        return kernel.apply(extent: inputImage.extent, arguments: inputs)
    }
}

extension UIImage {
    func colorized(with color: UIColor) -> UIImage {
        guard let cgInput = self.cgImage else {
            return self
        }
        let colorFilter = ColorFilter()
        colorFilter.inputImage = CIImage(cgImage: cgInput)
        colorFilter.inputColor = CIColor(color: color)

        if let ciOutputImage = colorFilter.outputImage {
            let context = CIContext(options: nil)
            let cgImg = context.createCGImage(ciOutputImage, from: ciOutputImage.extent)
            return UIImage(cgImage: cgImg!, scale: self.scale, orientation: self.imageOrientation).alpha(color.rgba.alpha).withRenderingMode(self.renderingMode)
        } else {
            return self
        }
    }

此时您应该已经拥有了一切来使其工作:

extension UIImage {
    func stroked(with color: UIColor, size: CGFloat) -> UIImage {
        let strokeImage = self.colorized(with: color)
        let oldRect = CGRect(x: size, y: size, width: self.size.width, height: self.size.height).integral
        let newSize = CGSize(width: self.size.width + (2*size), height: self.size.height + (2*size))
        let translationVector = CGPoint(x: size, y: 0)

        UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
        if let context = UIGraphicsGetCurrentContext() {
            context.interpolationQuality = .high

            let step = 10 // reduce the step to increase quality
            for angle in stride(from: 0, to: 360, by: step) {
                let vector = translationVector.rotated(around: .zero, byDegrees: CGFloat(angle))
                let transform = CGAffineTransform(translationX: vector.x, y: vector.y)
                context.concatenate(transform)
                context.draw(strokeImage.cgImage!, in: oldRect)
                let resetTransform = CGAffineTransform(translationX: -vector.x, y: -vector.y)
                context.concatenate(resetTransform)
            }
            context.draw(self.cgImage!, in: oldRect)

            let newImage = UIImage(cgImage: context.makeImage()!, scale: self.scale, orientation: self.imageOrientation)
            UIGraphicsEndImageContext()

            return newImage.withRenderingMode(self.renderingMode)
        }
        UIGraphicsEndImageContext()
        return self
    }
}

如果我们手动去除背景,边缘不会很平滑。您能否提供有关平滑边框的任何技术建议? - Ahmed
@Ahmed:“像素RGB越接近您想要擦除的颜色,像素就越透明”。我还不知道如何在算法中建模其数学表示。 - herme5
@herme5 所以如果我理解正确,你所做的就是将源图像扩展“边框图像”,使其比原始图像厚一些? - Esko918
1
@Esko918 就这样 - herme5
@herme5 谢谢你,伙计。 - Esko918
显示剩余2条评论

0

-4
使用这个简单的UIImage扩展。
extension UIImage {

    func outline() -> UIImage? {

        UIGraphicsBeginImageContext(size)
        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        self.draw(in: rect, blendMode: .normal, alpha: 1.0)
        let context = UIGraphicsGetCurrentContext()
        context?.setStrokeColor(red: 1.0, green: 0.5, blue: 1.0, alpha: 1.0)
        context?.setLineWidth(5.0)
        context?.stroke(rect)
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return newImage

    }

}

它将会给你一个带有粉色边框的图片。


我试用了你的代码:let image = UIImage(named: "plum")!.outline(),但是仍然在图片视图周围得到了边框,而不是图片本身! - Roman
不是围绕在ImageView上,而是围绕在图片周围。做一件事情,将你的ImageView放大,并保持其contentMode属性设置为aspect fit,并给它一个背景颜色。你会看到边框围绕着图片,而不是围绕着ImageView。 - Arun Kumar
嗨,Arun,我正在尝试按照你的建议操作,但结果并不是正确的边框。你能否提供一个简短的函数代码,而不仅仅是扩展名? - Roman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接