Search code examples
objective-cmacosbordernsimagecifilter

How to create a border around an NSImage CIFilter ObjC


I'm trying to create a stoke border around a PNG with transparent background in an NSImage.

I originally tried duplicating the image and having a larger scale and then using CIFilter to make it white - this works great for shapes like circles and other solid shapes.

However i'll have shapes like the example image below:

Tree Image

With this Image it doesn't work well at all.

I'm thinking that maybe I can do something with CIEdge and CIMorphologyMaximum but i'm not sure if i'm thinking in a good direction - would appreciate some advise if anyone has come across a similar challenge.


Solution

  • Yes, CIMorphologyMaximum should be the way to go. Try this:

    import CoreImage.CIFilterBuiltins
    
    let ciImage = ...
    
    // Apply morphology maximum to "erode" image in all direction into transparent area.
    let filter = CIFilter.morphologyMaximum()
    filter.inputImage = ciImage
    filter.radius = 5 // border radius
    let eroded = filter.outputImage!
    
    // Turn all pixels of eroded image into desired border color.
    let colorized = CIBlendKernel.sourceAtop.apply(foreground: .white, background: eroded)!.cropped(to: eroded.extent)
    
    // Blend original image over eroded, colorized image.
    let imageWithBorder = ciImage.composited(over: colorized)
    

    And in Objective-C:

    CIImage* ciImage = ...;
    
    // Apply morphology maximum to "erode" image in all direction into transparent area.
    CIFilter* erodeFilter = [CIFilter filterWithName:@"CIMorphologyMaximum"];
    [erodeFilter setValue:ciImage forKey:kCIInputImageKey];
    [erodeFilter setValue:@5 forKey:kCIInputRadiusKey];
    CIImage* eroded = erodeFilter.outputImage;
    
    // Turn all pixels of eroded image into desired border color.
    CIImage* colorized = [[CIBlendKernel.sourceAtop applyWithForeground:[CIImage whiteImage] background:eroded] imageByCroppingToRect:eroded.extent];
    
    // Blend original image over eroded, colorized image.
    CIImage* imageWithBorder = [ciImage imageByCompositingOverImage:colorized];
    

    Keep in mind that the border will extend the image in all directions which might result in a negative origin in working space. For example, an image with extent [0, 0, 50, 50] will have an extent of [-5, -5, 60, 60] after applying a 5 pixel border.

    To compensate for that, you need to specify the extent of the resulting image when as the rect when rendering the image:

    [ciContext createCGImage:imageWithBorder fromRect:imageWithBorder.extent];
    

    Alternatively, you can move the image's origin to [0, 0] again after applying the border. But the resulting will still be larger than the input image, so keep that in mind.

    imageWithBorder = [imageWithBorder imageByApplyingTransform:CGAffineTransformMakeTranslation(-ciImage.extent.origin.x, -ciImage.extent.origin.y)];
    

    As for the border size: The inputRadius you set on the erosion filter is in pixels. That means that large images will get a smaller relative border compared to small images.

    To compensate for that, you can calculate the border radius as a percentile of the image size. This should create a uniform look among differently sized images. For instance:

    NSNumber* radius = @(MAX(ciImage.extent.size.width, ciImage.extent.size.height) * 0.05);