Search code examples
swiftxcodemacoscore-imagecifilter

How can I get a custom CIFilter to work on a CALayer for export using the AVVideoCompositionCoreAnimationTool


When using a custom CIFilter that relies on custom input parameters the parameters are not used once you set the filter to the CALayer backgroundFilters so you can render it as a video using AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer(s):) the input parameters are no longer used, it looks like CALayer creates a new CIFilter and does not add the set parameters.

Using the default options:

let layer = CALayer()
let filter = CIFilter(name: "CustomFilter")!
filter.setValue(10, forKey: "value")
layer.backgroundFilters = [filter]

or:

let layer = CALayer()
let filter = CIFilter(name: "CustomFilter", options: ["value": 10])!
layer.backgroundFilters = [filter]

or using what Apple mentions in the manual:

let layer = CALayer()
let filter = CIFilter(name: "CustomFilter")!
filter.name = "myFilter"
layer.backgroundFilters = [filter]
layer.setValue(10, forKeyPath: "backgroundFilters.myFilter.value"

No matter what order, what values, if the custom filter is defined like this:

class CustomFilter: CIFilter {
    override class var supportsSecureCoding: Bool {
        return true
    }

    
    @objc dynamic var inputImage: CIImage?
    @objc dynamic var value: NSNumber?

    override public var outputImage: CIImage? {
        guard let image = inputImage else {
            return nil
        }

        // return the actual stuff you want done here!
    }
}

and even registered using:

class CustomFilterRegister: CIFilterConstructor {
    static func register() {
        CIFilter.registerName(
            "CustomFilter", constructor: CustomFilterRegister(),
            classAttributes: [
                kCIAttributeFilterCategories: [
                    kCICategoryVideo,
                    kCICategoryStillImage,
                    "CustomFilters"
                ],
            ])
    }
    
    func filter(withName name: String) -> CIFilter? {
        switch name {
        case "CustomFilter":
            return CustomFilter()
        default:
            return nil
        }
    }
}

The CALayer is NOT affected by the setting in the value param.


Solution

  • There seem to be 2 things that are important to add in order for CALayers to work with the custom CIFilter.

    1. You need to override the inputKeys:
    override var inputKeys: [String] {
       return [kCIInputImageKey, "value"]
    }
    
    1. You need to declare all used parameters like this:
    override var attributes: [String : Any] {
      return [
          kCIAttributeFilterDisplayName: "Custom Filter",
          kCIAttributeFilterName: "CustomFilter",
          kCIAttributeFilterCategories: [
              kCICategoryVideo,
              kCICategoryStillImage,
              "CustomFilters",
          ],
          kCIInputImageKey: [
              kCIAttributeIdentity: 0,
              kCIAttributeClass: "CIImage",
              kCIAttributeDisplayName: "Image",
              kCIAttributeType: kCIAttributeTypeImage
          ],
          #keyPath(value): [
              kCIAttributeIdentity: 0,
              kCIAttributeClass: "NSNumber",
              kCIAttributeDisplayName: "Custom value",
              kCIAttributeMin: 0,
              kCIAttributeMax: 5,
              kCIAttributeSliderMin: 0,
              kCIAttributeSliderMax: 5,
              kCIAttributeType: kCIAttributeTypeScalar
          ],
          kCIOutputImageKey: [
              kCIAttributeClass: "CIImage",
              kCIAttributeDisplayName: "Output Image",
              kCIAttributeType: kCIAttributeTypeImage
          ]
      ]
    }
    

    Hope I save some of you loads of time figuring this one out!