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)
return nil
} else {
return Task.detached(priority: .userInitiated) {
let result = await self.fetch(url: url)
switch result {
case .data(let data):
completion?(data, nil)
case .dataAndImage(let data, let image):
completion?(data, image)
case .none:
completion?(nil, nil)
}
return getFromSource(url, completion: completion)
}
}
func getFromSource(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
return Task.detached(priority: .userInitiated) {
let result = await self.fetch(url: url)
switch result {
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 {
// 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) {
let adjustedCapHeight = font.capHeight - 1
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)

View File

@ -40,6 +40,16 @@ extension BaseEmojiLabel {
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>()
var foundEmojis = false
@ -57,21 +67,35 @@ extension BaseEmojiLabel {
foundEmojis = true
if let image = ImageCache.emojis.get(URL(emoji.url)!)?.image {
// if the image is cached, add it immediately
emojiImages[emoji.shortcode] = image
// if the image is cached, add it immediately.
// 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 {
// otherwise, perform the network request
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.get(URL(emoji.url)!) { (_, image) in
guard let image = image,
let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: image) else {
group.leave()
return
let request = ImageCache.emojis.getFromSource(URL(emoji.url)!) { (_, image) in
guard let image else {
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 {
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)
// so, just ignore the warnings
let emojiAttachments = emojiImages.withLock {
let emojiFont = self.emojiFont
let emojiTextColor = self.emojiTextColor
return $0.mapValues { image in
NSTextAttachment(emojiImage: image, in: emojiFont, with: emojiTextColor)
NSTextAttachment(image: image)
}
}
let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil