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
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.
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!