how to add colored border to uiimage in swift
Asked Answered
M

4

16

It is pretty easy to add border to UIImageView, using layers (borderWidth, borderColor etc.). Is there any possibility to add border to image, not to image view? Does somebody know?

Update:

I tried to follow the suggestion below und used extension. Thank you for that but I did not get the desired result. Here is my code. What is wrong?

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

The second image with the red border is more or less what I need:

enter image description here

enter image description here

Maraud answered 20/12, 2017 at 6:46 Comment(4)
What exactly do you need here?Grizzly
I need, let say, a red border with borderWidth 3 around the plum itself and not around the image view. How could I get it?Maraud
I added a second picture with the more or less desirable result.Maraud
Any solutions @romanWeld
P
26

Strongly inspired by @herme5, refactored into more compact Swift 5/iOS12+ code as follows (fixed vertical flip issue as well):

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

Polygynist answered 24/4, 2020 at 17:8 Comment(15)
You forgot to include the CGPoint.rotated func.Lelalelah
Sure. I wanted to try your code for outlining country flags but the outline at the top is missing. Basically, after calling stroked the UIImage has an outline at the bottom and at the sides. Any clue as to why this might be happening? I'm using it on a vanilla UIImageView with flags urls taken from wikipedia.Lelalelah
If anyone is looking for something simpler (rectangular UIImage / simple borders) these answers might be sufficient.Lelalelah
This was so helpful than you so much! However, this line: context.translateBy(x: 0, y: size.height) Should be: context.translateBy(x: 0, y: newSize.height)Valerivaleria
Great solution, I've suggested an edit which fixes a centering issue after translation. Please approve itUncounted
Thanks Husam, thanks to have pointed that. Wrongily, the addition wasn't included in my original post (copy/paste issue). So thanks again for the fix! 👍🏻Advertence
Btw, how can I approve the fix? I don't see any cal to action for that @Husam?Advertence
@StéphanedeLuca, I didn't notice that my edits doesn't need a peer approval. BTW you have changed the wrong size object in ur last edit, I've edited it again, you can check the history of edits, Everything should be fine now. BR.Uncounted
Can anyone translate to ObjC pls?Rheotaxis
For me, it's not doing much. If I increase the thickness, it's setting a boundary on the inside edge of the image.Tolerate
Great!! I googled a lot and your way is the best solution!Ticklish
Anyone got a version that works with NSImage for macOS?Confectioner
Thank you man this worked perfectly for my iconsCarnage
This solution didn't work for me as the image it returned was rotated and stretched. I wound up using this solution instead: stackoverflow.com/a/61552479Melodimelodia
Nice solution, unfortunately this changes the image size.Heinrick
S
14

Here is a UIImage extension I wrote in Swift 4. As IOSDealBreaker said this is all about image processing, and some particular cases may occur. You should have a png image with a transparent background, and manage the size if larger than the original.

  • First get a colorised "shade" version of your image.
  • Then draw and redraw the shade image all around a given origin point (In our case around (0,0) at a distance that is the border thickness)
  • Draw your source image at the origin point so that it appears on the foreground.
  • You may have to enlarge your image if the borders go out of the original rect.

My method uses a lot of util methods and class extensions. Here is some maths to rotate a vector (which is actually a point) around another point: 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)
    }
}

I wrote my custom CIFilter to colorise an image which have a transparent background: Colorize a UIImage in Swift

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

At this point you should have everything to make this work:

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
    }
}
Schnook answered 12/11, 2018 at 11:5 Comment(7)
If we manually remove background the edges are not smooth. can you please suggest any technique about smoothing the border?Ozonize
@Ahmed, short answer: Photo editing software like Gimp or Photoshop. I don't think the perfect algorithm exists for all photos (especially realistic ones, with landscapes).Schnook
@Ozonize Nevertheless I have this method for images that are relatively simple (e.g: monochromatic logos typos cartoon-drawing with uniform background) Gimp has a filter that "erase" a color channel (e.g: every white pixels will become fully transparent). Take a look at this question: #50217113 you could rewrite a shader function so that: the more the pixel RGB is close to the color you want to erase, the less is the alpha.Schnook
@Ozonize "The more the pixel RGB is close to the color you want to erase, the more the pixel is transparent". I don't know yet how you can model its mathematical representation in an algorithm.Schnook
@Schnook so if i understand this correctly all your really doing is taking the source image and expanding the "border image" some amount of thickness larger than the original image?Carnage
@Carnage that’s itSchnook
@Schnook thank you dudeCarnage
M
0

Borders to the images belongs to image processing area of iOS. It's not easy as borders for a UIView, It's pretty deep but if you're willing to go the distance, here is a library and a hint for the journey https://github.com/BradLarson/GPUImage

try using GPUImageThresholdEdgeDetectionFilter

or try OpenCV https://docs.opencv.org/2.4/doc/tutorials/ios/image_manipulation/image_manipulation.html

Measureless answered 20/12, 2017 at 6:50 Comment(0)
M
-4

Use this simple extension for 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

    }

}

It will give you an image with pink border.

Morn answered 20/12, 2017 at 7:4 Comment(3)
I tried your code with: let image = UIImage(named: "plum")!.outline() and still get the border arounf the imge view and not around the image itself!Maraud
Its around the image not the image view. Do one thing , Make your imageview bigger and keep its content mode property set to aspect fit and give it a background color. You will se the border around the image, not around the imageView.Morn
Hi Arun, I am trying to follow your suggestion but the result is not the right border. Could you give a short code for func perhaps, not only the extention?Maraud

© 2022 - 2024 — McMap. All rights reserved.