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:
parent
91ef386a41
commit
8319935a3d
|
@ -37,6 +37,11 @@ class ImageCache {
|
||||||
completion?(entry.data, entry.image)
|
completion?(entry.data, entry.image)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
|
return getFromSource(url, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFromSource(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
|
||||||
return Task.detached(priority: .userInitiated) {
|
return Task.detached(priority: .userInitiated) {
|
||||||
let result = await self.fetch(url: url)
|
let result = await self.fetch(url: url)
|
||||||
switch result {
|
switch result {
|
||||||
|
@ -49,7 +54,6 @@ class ImageCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
|
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
|
||||||
if !ImageCache.disableCaching,
|
if !ImageCache.disableCaching,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,22 +67,36 @@ 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
|
||||||
|
}
|
||||||
|
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()
|
group.leave()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
emojiImages[emoji.shortcode] = transformedImage
|
emojiImages[emoji.shortcode] = transformedImage
|
||||||
group.leave()
|
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
|
||||||
|
|
Loading…
Reference in New Issue