Realistic Text Emboss Effect in iOS

In my most recent project, I had to solve the problem of rendering text on a credit card’s background in a realistic, pleasant way.  The client was a bank and cards are a strong part of a bank’s corporate identity.  The text is embossed with complex lighting and shading effects, like this:


It turns out that iOS has a couple of useful filters that can be used, with the appropriate texture, to obtain just that, namely CIHeightFieldFromMask  and CIShadedMaterial .

Let’s make a single view app, with Storyboard.  I have called mine “Emboss”.  Don’t change any of the default configurations and names that come with the Xcode template for a single view app (as of version 8.1, March 2017).

The problem with filter is the they can only be applied to an image so the first problem we need to solve is how to render a text into an UIImage .  This can be accomplished by the following extension:

extension UIImage {
    class func image(from string: String, attributes: [String: Any]?, size: CGSize) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        string.draw(with: rect, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        return image

Please not that this method takes as an input both your text as a String  and its attributes as a Dictionary .  The last parameter (a CGSize ) represents the rectangle the string needs to be rendered into.  It is worth noting that the returned object is a UIImage  backed by a CGImage .  This is important, not every UIImage  is backed by a CGImage , some are backed by a CIImage .

Now that we have a way to turn a text into images, we can apply the two filters mentioned above, in the correct order and with an arbitrary texture.  Once again we favour an extension to achieve that.

extension UIImage {
    func applyEmboss(shadingImage: UIImage) -> UIImage {
        // Create filters
        guard let heightMapFilter = CIFilter(name: "CIHeightFieldFromMask") else { return self }
        guard let shadedMaterialFilter = CIFilter(name: "CIShadedMaterial") else { return self }
        // Filters chain
        heightMapFilter.setValue(CIImage(image: self),
                                 forKey: kCIInputImageKey)
        guard let heightMapFilterOutput = heightMapFilter.outputImage else { return self }
                                      forKey: kCIInputImageKey)
        shadedMaterialFilter.setValue(CIImage(image: shadingImage),
                                      forKey: "inputShadingImage")
        // Catch output
        guard let filteredImage = shadedMaterialFilter.outputImage else { return self }
        return UIImage(ciImage: filteredImage)

Note that the shading (or texture) image is arbitrary.  The output is a UIImage  backed by a CIImage .  This is important, you cannot extract directly a CGImage  from this output.  If you call myOutputImage.cgImage  the result will be nil.  If you, on the other hand call myOutputImage.ciImage  the result will return the correct CIImage .  If you want to write generic code, this has implications on how functions are chained (we will not discuss or develop further this topic in the current post).

Let’s put it all together.  Add an image view in the main storyboard and pin it to the top and bottom layout guides, left and right margins.  Connect the image view wth an outlet your view controller (call it imageView ).  You should end up with something like this:

Main Storyboard configuration

In order to apply the emboss effect you need a texture and this is where the magic really happens.  Generally speaking you will want a rasterised version of a 3D sphere on a black background. This will represent the material you text will be made of. The position of lights and shades will make your effect as beautiful or as ugly are it deserves. Download the following texture:


In your Assets.xcassets  make a new image set (call it “texture1”) and add the above texture in the 3x slot.

And finally your ViewController .  Given that converting a string with attributes into text requires a rectangle, we need to trigger the rendering after the views have been laid out.  The key method is viewDidLayoutSubviews .  Here is the full view controller:

class ViewController: UIViewController {
    // Play around with these parameters and build for device.
    private let inputText = "Emboss me like a boss"
    private let embossTexture = UIImage(named: "texture1")!
    private let fontSize: CGFloat = 40
    @IBOutlet weak var imageView: UIImageView!
    override func viewDidLayoutSubviews() {
        let font = UIFont.boldSystemFont(ofSize: fontSize)
        let stringAttributes = [NSFontAttributeName: font,
                                NSForegroundColorAttributeName: UIColor.white]
        let size = imageView.frame.size
        imageView.image = UIImage
            .image(from: inputText, attributes: stringAttributes, size: size)?
            .applyEmboss(shadingImage: embossTexture)


As simple as that!  Please note that this code is missing, as stated above, the ability to arbitrarily chain the filter with other operations that require a CGImage , so in that respect the code is a little dirty and would require further work.

You can find the full little mini project here (GitHub).  Did you enjoy? Leave us a nice comment!


  • The technique shown above is a much simplified version of this repository by FlexMonkey.
  • How did I obtain the texture used in this project?
  • I have made a dirty port of FlexMonkey’s code to swift 3 and modified the code in order be able to produce textures of different colours (including black and white) and to save such textures in a .png  file.
  • Alternatively, if you can, play around with a more sophisticated 3D rendering software and see what happens.  Happy experimenting!

This Post Has One Comment

  1. Paul

    very cool. thanks.
    btw: to get this to work on OSX I had to make a 1x version of the image too. otherwise NSImage(named:) wouldn’t load…

Leave a Reply