Search code examples
swiftcalayeravkitcatextlayeravvideocomposition

CATextLayer on video pixelated text


I am creating video from images and adding overlay to them. Problem is when I try to add CATextLayer to video. Text is pixelated, take a look at the image

image

This is the code used to generate CATextLayer:

private func generateTextLayer(for text: String, at frame: CGRect, in layer: CALayer) -> CATextLayer {
    let textLayer = CATextLayer()

    textLayer.frame = frame.integral
    textLayer.contentsScale = UIScreen.main.scale

    textLayer.isWrapped = true
    textLayer.isHidden = true

    textLayer.foregroundColor = UIColor.white.cgColor
    textLayer.backgroundColor = UIColor.clear.cgColor

    let fontHeight = resolution.fontHeight
    textLayer.font = UIFont.systemFont(ofSize: fontHeight)
    textLayer.fontSize = fontHeight

    textLayer.string = text

    textLayer.truncationMode = .end
    textLayer.alignmentMode = .center

    textLayer.contentsScale = UIScreen.main.scale

    layer.addSublayer(textLayer)

    return textLayer
}

And this code is used to generate CALayer with image:

private func generateImageLayer(for image: CGImage?, at frame: CGRect, in layer: CALayer) -> CALayer {
    let imageLayer = CALayer()
    imageLayer.frame = frame.integral

    imageLayer.contents = image
    imageLayer.contentsGravity = .resizeAspectFill

    imageLayer.isHidden = true

    imageLayer.backgroundColor = UIColor.clear.cgColor

    layer.addSublayer(imageLayer)

    return imageLayer
}

Image layer is hidden due to animation of different image layers in generated video. This is code used for layer generation:

    let size = backgroundTrack.naturalSize.applying(backgroundTrack.preferredTransform)

    let parentlayer = CALayer()
    parentlayer.frame = CGRect(origin: .zero, size: size).integral
    parentlayer.backgroundColor = UIColor.black.cgColor
    parentlayer.isOpaque = true

    let backgroundVideoLayer = CALayer()
    backgroundVideoLayer.frame = parentlayer.bounds
    backgroundVideoLayer.backgroundColor = UIColor.black.cgColor
    backgroundVideoLayer.isOpaque = true

    parentlayer.addSublayer(backgroundVideoLayer)

    var currentTime: Double = 0
    for source in videoSources {

        // This will generate blurred background layer
        let blurrLayer = generateImageLayer(for: source.blurredImage, at: backgroundVideoLayer.bounds, in: backgroundVideoLayer)
        blurrLayer.contentsGravity = .resizeAspectFill

        // Adds image to blurrLayer
        if let image = source.image { 
            let frame = calculateFrame(for: image.size)
            let imageLayer = generateImageLayer(for: image.cgImage, at: frame, in: blurrLayer)
            imageLayer.isHidden = false

            Animations.fadeInOut(layer: blurrLayer, beginTime: currentTime, duration: duration.value)
        }

        // Adds CATextLayer 
        if let text = source.title {
            let frame = parentlayer.bounds.offsetBy(dx: 0, dy: 100)

            let titleLayer = generateTextLayer(for: text, at: frame, in: parentlayer)

            Animations.fadeInOut(layer: titleLayer, beginTime: currentTime, duration: duration.value)
        }

        currentTime += duration.value
    }

I tried setting rasterizationScale to UIScreen.main.scale, shouldRasterize to true, contentsScale to UIScreen.main.scale, all kind of different options, even tried snapshoting UILabel and setting it as image (even saved image locally and checked it, quality was good), but result is again pixelated writing.

Animate fadeInOut is basically three animations, one is setting hidden param to true or false and another one is adding CATransition reveal animation for entry and exit transition between layers.

    static func fadeInOut(layer: CALayer, beginTime: Double, duration: Double, skipEntry: Bool = false) {
    let t = beginTime < 1 ? -0.0001 : beginTime
    let d = beginTime < 1 ? duration + 0.0001 : duration - 0.0001

    if !skipEntry {
        let entryTransition = CATransition()

        entryTransition.beginTime = t
        entryTransition.duration = 0.5

        entryTransition.type = CATransitionType.reveal
        entryTransition.subtype = CATransitionSubtype.fromTop

        entryTransition.isRemovedOnCompletion = false
        layer.add(entryTransition, forKey: "entry")
    }

    let exitTransition = CATransition()

    exitTransition.beginTime = d - 0.5
    exitTransition.duration = 0.5

    exitTransition.startProgress = 1
    exitTransition.endProgress = 0

    exitTransition.type = CATransitionType.reveal
    exitTransition.subtype = CATransitionSubtype.fromTop

    exitTransition.isRemovedOnCompletion = false
    layer.add(exitTransition, forKey: "exit")

    let fadeInOutAnimation = CABasicAnimation(keyPath: "hidden")
    fadeInOutAnimation.fromValue = false
    fadeInOutAnimation.toValue = false

    fadeInOutAnimation.beginTime = t
    fadeInOutAnimation.duration = d

    fadeInOutAnimation.isRemovedOnCompletion = false
    layer.add(fadeInOutAnimation, forKey: "opacity")
}

Before marking my question as already seen and answered, I've spent lot of time trying to google possible results and tried many things, but results are still the same.


Solution

  • So in order to get good quality image or text while generating video, one must pay attention to the contentScale to set it to 1. Other issue that I was having would be problem with some frames, where UIImageView or UITextField could scale everything to look sharp while contentMode would take care of everything, while working with video logic to generate video dev must take extra care about this

    What I mean by this is iff image is bigger from layer frame, image will scale down but it will look pixelated. Giving the layer same frame as image will make image look good. This is especially visible on watermark images, where we have put image to Assets with 1x, 2x and 3x dimension. After getting this image with UIImage(named: "my_watermark") it would load 3x dimension, and we needed 1x dimension for video because of video scale == 1. Removing the 2x and 3x dimension sorted this issue.

    When we found out this, we proceeded with same tests for CATextLayer, so setting contentScale to 1, and taking care of frame, did the trick.

    Video generated by following this logic had good quality of both texts and images inside. The biggest issue was actually time needed to check output results after playing with settings, where generating video would take some time, and then transferring it to bigger screen to check everything was time consuming.

    Hope this answer helps someone!