BaseEmojiLabel improvements

Avoid rechecking disk/memory caches when fetching

Use UIImage thumbnail API, rather than UIGraphicsImageRenderer, and make
thumbnail off main thread when possible
This commit is contained in:
Shadowfacts 2023-05-14 15:19:00 -04:00
parent 91ef386a41
commit 8319935a3d
3 changed files with 50 additions and 23 deletions

View File

@ -37,16 +37,20 @@ class ImageCache {
completion?(entry.data, entry.image) completion?(entry.data, entry.image)
return nil return nil
} else { } else {
return Task.detached(priority: .userInitiated) { return getFromSource(url, completion: completion)
let result = await self.fetch(url: url) }
switch result { }
case .data(let data):
completion?(data, nil) func getFromSource(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
case .dataAndImage(let data, let image): return Task.detached(priority: .userInitiated) {
completion?(data, image) let result = await self.fetch(url: url)
case .none: switch result {
completion?(nil, nil) case .data(let data):
} completion?(data, nil)
case .dataAndImage(let data, let image):
completion?(data, image)
case .none:
completion?(nil, nil)
} }
} }
} }

View File

@ -10,6 +10,7 @@ import UIKit
extension NSTextAttachment { extension NSTextAttachment {
// Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m // Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
@available(iOS, deprecated: 15.0)
convenience init(emojiImage image: UIImage, in font: UIFont, with textColor: UIColor = .label) { convenience init(emojiImage image: UIImage, in font: UIFont, with textColor: UIColor = .label) {
let adjustedCapHeight = font.capHeight - 1 let adjustedCapHeight = font.capHeight - 1
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight) var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)

View File

@ -40,6 +40,16 @@ extension BaseEmojiLabel {
return return
} }
// Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
let adjustedCapHeight = emojiFont.capHeight - 1
func emojiImageSize(_ image: UIImage) -> CGSize {
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
var scale: CGFloat = 1.4
scale *= UIScreen.main.scale
imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * scale, height: imageSizeMatchingFontSize.height * scale)
return imageSizeMatchingFontSize
}
let emojiImages = MultiThreadDictionary<String, UIImage>() let emojiImages = MultiThreadDictionary<String, UIImage>()
var foundEmojis = false var foundEmojis = false
@ -57,21 +67,35 @@ extension BaseEmojiLabel {
foundEmojis = true foundEmojis = true
if let image = ImageCache.emojis.get(URL(emoji.url)!)?.image { if let image = ImageCache.emojis.get(URL(emoji.url)!)?.image {
// if the image is cached, add it immediately // if the image is cached, add it immediately.
emojiImages[emoji.shortcode] = image // we generate the thumbnail on the main thread, because it's usually fast enough
// and the delay caused by doing it asynchronously looks works.
// todo: consider caching these somewhere? the cache needs to take into account both the url and the font size, which may vary across labels, so can't just use ImageCache
if let thumbnail = image.preparingThumbnail(of: emojiImageSize(image)),
let cgImage = thumbnail.cgImage {
// the thumbnail API takes a pixel size and returns an image with scale 1, but we want the actual screen scale, so convert
// see FB12187798
emojiImages[emoji.shortcode] = UIImage(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .up)
}
} else { } else {
// otherwise, perform the network request // otherwise, perform the network request
group.enter() group.enter()
// todo: ImageCache.emojis.get here will re-check the memory and disk caches, there should be another method to force-refetch let request = ImageCache.emojis.getFromSource(URL(emoji.url)!) { (_, image) in
let request = ImageCache.emojis.get(URL(emoji.url)!) { (_, image) in guard let image else {
guard let image = image, group.leave()
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: image) else { return
group.leave() }
return image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in
guard let thumbnail = thumbnail?.cgImage,
case let rescaled = UIImage(cgImage: thumbnail, scale: UIScreen.main.scale, orientation: .up),
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: rescaled) else {
group.leave()
return
}
emojiImages[emoji.shortcode] = transformedImage
group.leave()
} }
emojiImages[emoji.shortcode] = transformedImage
group.leave()
} }
if let request = request { if let request = request {
emojiRequests.append(request) emojiRequests.append(request)
@ -91,10 +115,8 @@ extension BaseEmojiLabel {
// even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878) // even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878)
// so, just ignore the warnings // so, just ignore the warnings
let emojiAttachments = emojiImages.withLock { let emojiAttachments = emojiImages.withLock {
let emojiFont = self.emojiFont
let emojiTextColor = self.emojiTextColor
return $0.mapValues { image in return $0.mapValues { image in
NSTextAttachment(emojiImage: image, in: emojiFont, with: emojiTextColor) NSTextAttachment(image: image)
} }
} }
let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil