Emoji input improvements

This commit is contained in:
Ali
2022-07-15 03:37:03 +02:00
parent 0b872d86c5
commit 0577baac79
35 changed files with 1413 additions and 694 deletions

View File

@@ -15,7 +15,7 @@ private func alignUp(size: Int, align: Int) -> Int {
public final class AnimationCacheItemFrame {
public enum RequestedFormat {
case rgba
case yuva(bytesPerRow: Int)
case yuva(rowAlignment: Int)
}
public final class Plane {
@@ -50,11 +50,17 @@ public final class AnimationCacheItem {
public let numFrames: Int
private let getFrameImpl: (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?
private let getFrameIndexImpl: (Double) -> Int
private let getFrameDurationImpl: (Int) -> Double?
public init(numFrames: Int, getFrame: @escaping (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?, getFrameIndexImpl: @escaping (Double) -> Int) {
public init(numFrames: Int, getFrame: @escaping (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?, getFrameIndexImpl: @escaping (Double) -> Int, getFrameDurationImpl: @escaping (Int) -> Double?) {
self.numFrames = numFrames
self.getFrameImpl = getFrame
self.getFrameIndexImpl = getFrameIndexImpl
self.getFrameDurationImpl = getFrameDurationImpl
}
public func getFrameDuration(index: Int) -> Double? {
return self.getFrameDurationImpl(index)
}
public func getFrame(index: Int, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? {
@@ -123,7 +129,7 @@ private func md5Hash(_ string: String) -> String {
}
}
private func itemSubpath(hashString: String) -> (directory: String, fileName: String) {
private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) {
assert(hashString.count == 32)
var directory = ""
@@ -134,7 +140,7 @@ private func itemSubpath(hashString: String) -> (directory: String, fileName: St
directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)]))
}
return (directory, hashString)
return (directory, "\(hashString)_\(width)x\(height)")
}
private func roundUp(_ numToRound: Int, multiple: Int) -> Int {
@@ -209,6 +215,213 @@ private func decompressData(data: Data, range: Range<Int>, decompressedSize: Int
return decompressedFrameData
}
private final class AnimationCacheItemWriterInternal {
struct CompressedResult {
var path: String
}
private struct FrameMetadata {
var offset: Int
var length: Int
var duration: Double
}
var isCancelled: Bool = false
private let decompressedPath: String
private let compressedPath: String
private var file: ManagedFile?
private var currentYUVASurface: ImageYUVA420?
private var currentDctData: DctData?
private var currentDctCoefficients: DctCoefficientsYUVA420?
private var contentLengthOffset: Int?
private var isFailed: Bool = false
private var isFinished: Bool = false
private var frames: [FrameMetadata] = []
private var contentLength: Int = 0
private let dctQuality: Int
init?(allocateTempFile: @escaping () -> String) {
self.dctQuality = 70
self.decompressedPath = allocateTempFile()
self.compressedPath = allocateTempFile()
guard let file = ManagedFile(queue: nil, path: self.decompressedPath, mode: .readwrite) else {
return nil
}
self.file = file
}
func add(with drawingBlock: (ImageYUVA420) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) {
if self.isFailed || self.isFinished {
return
}
guard !self.isFailed, !self.isFinished, let file = self.file else {
return
}
let width = roundUp(proposedWidth, multiple: 16)
let height = roundUp(proposedWidth, multiple: 16)
var isFirstFrame = false
let yuvaSurface: ImageYUVA420
if let current = self.currentYUVASurface {
if current.yPlane.width == width && current.yPlane.height == height {
yuvaSurface = current
} else {
self.isFailed = true
return
}
} else {
isFirstFrame = true
yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil)
self.currentYUVASurface = yuvaSurface
}
let dctCoefficients: DctCoefficientsYUVA420
if let current = self.currentDctCoefficients {
if current.yPlane.width == width && current.yPlane.height == height {
dctCoefficients = current
} else {
self.isFailed = true
return
}
} else {
dctCoefficients = DctCoefficientsYUVA420(width: width, height: height)
self.currentDctCoefficients = dctCoefficients
}
let dctData: DctData
if let current = self.currentDctData, current.quality == self.dctQuality {
dctData = current
} else {
dctData = DctData(quality: self.dctQuality)
self.currentDctData = dctData
}
drawingBlock(yuvaSurface)
yuvaSurface.dct(dctData: dctData, target: dctCoefficients)
if isFirstFrame {
file.write(2 as UInt32)
file.write(UInt32(dctCoefficients.yPlane.width))
file.write(UInt32(dctCoefficients.yPlane.height))
file.write(UInt32(dctData.quality))
self.contentLengthOffset = Int(file.position())
file.write(0 as UInt32)
}
let framePosition = Int(file.position())
assert(framePosition >= 0)
var frameLength = 0
for i in 0 ..< 4 {
let dctPlane: DctCoefficientPlane
switch i {
case 0:
dctPlane = dctCoefficients.yPlane
case 1:
dctPlane = dctCoefficients.uPlane
case 2:
dctPlane = dctCoefficients.vPlane
case 3:
dctPlane = dctCoefficients.aPlane
default:
preconditionFailure()
}
dctPlane.data.withUnsafeBytes { bytes in
let _ = file.write(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count)
}
frameLength += dctPlane.data.count
}
self.frames.append(FrameMetadata(offset: framePosition, length: frameLength, duration: duration))
self.contentLength += frameLength
}
func finish() -> CompressedResult? {
var shouldComplete = false
outer: for _ in 0 ..< 1 {
if !self.isFinished {
self.isFinished = true
shouldComplete = true
guard let contentLengthOffset = self.contentLengthOffset, let file = self.file else {
self.isFailed = true
break outer
}
assert(contentLengthOffset >= 0)
let metadataPosition = file.position()
file.seek(position: Int64(contentLengthOffset))
file.write(UInt32(self.contentLength))
file.seek(position: metadataPosition)
file.write(UInt32(self.frames.count))
for frame in self.frames {
file.write(UInt32(frame.offset))
file.write(UInt32(frame.length))
file.write(Float32(frame.duration))
}
if !self.frames.isEmpty {
} else {
self.isFailed = true
break outer
}
if !self.isFailed {
self.file = nil
file._unsafeClose()
guard let uncompressedData = try? Data(contentsOf: URL(fileURLWithPath: self.decompressedPath), options: .alwaysMapped) else {
self.isFailed = true
break outer
}
guard let compressedData = compressData(data: uncompressedData) else {
self.isFailed = true
break outer
}
guard let compressedFile = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else {
self.isFailed = true
break outer
}
compressedFile.write(Int32(uncompressedData.count))
let _ = compressedFile.write(compressedData)
compressedFile._unsafeClose()
}
}
}
if shouldComplete {
let _ = try? FileManager.default.removeItem(atPath: self.decompressedPath)
if !self.isFailed {
return CompressedResult(path: self.compressedPath)
} else {
let _ = try? FileManager.default.removeItem(atPath: self.compressedPath)
return nil
}
} else {
return nil
}
}
}
private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
struct CompressedResult {
var animationPath: String
@@ -246,7 +459,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
private let lock = Lock()
init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) {
self.dctQuality = 67
self.dctQuality = 70
self.queue = queue
self.decompressedPath = allocateTempFile()
@@ -286,7 +499,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
} else {
isFirstFrame = true
surface = ImageARGB(width: width, height: height, bytesPerRow: alignUp(size: width * 4, align: 32))
surface = ImageARGB(width: width, height: height, rowAlignment: 32)
self.currentSurface = surface
}
@@ -299,7 +512,7 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
return
}
} else {
yuvaSurface = ImageYUVA420(width: width, height: height, bytesPerRow: nil)
yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil)
self.currentYUVASurface = yuvaSurface
}
@@ -556,17 +769,17 @@ private final class AnimationCacheItemAccessor {
if let currentYUVASurface = self.currentYUVASurface {
yuvaSurface = currentYUVASurface
} else {
yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, bytesPerRow: nil)
yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, rowAlignment: nil)
}
case let .yuva(preferredBytesPerRow):
yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, bytesPerRow: preferredBytesPerRow)
case let .yuva(preferredRowAlignment):
yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, rowAlignment: preferredRowAlignment)
}
self.currentDctCoefficients.idct(dctData: self.currentDctData, target: yuvaSurface)
switch requestedFormat {
case .rgba:
let currentSurface = ImageARGB(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height, bytesPerRow: alignUp(size: yuvaSurface.yPlane.width * 4, align: 32))
let currentSurface = ImageARGB(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height, rowAlignment: 32)
yuvaSurface.toARGB(target: currentSurface)
self.currentYUVASurface = yuvaSurface
@@ -619,6 +832,14 @@ private final class AnimationCacheItemAccessor {
}
return self.durationMapping.count - 1
}
func getFrameDuration(index: Int) -> Double? {
if index < self.durationMapping.count {
return self.durationMapping[index]
} else {
return nil
}
}
}
private func readUInt32(data: Data, offset: Int) -> UInt32 {
@@ -747,9 +968,100 @@ private func loadItem(path: String) -> AnimationCacheItem? {
return itemAccessor.getFrame(index: index, requestedFormat: requestedFormat)
}, getFrameIndexImpl: { duration in
return itemAccessor.getFrameIndex(duration: duration)
}, getFrameDurationImpl: { index in
return itemAccessor.getFrameDuration(index: index)
})
}
private func adaptItemFromHigherResolution(itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? {
guard let higherResolutionItem = loadItem(path: higherResolutionPath) else {
return nil
}
guard let writer = AnimationCacheItemWriterInternal(allocateTempFile: allocateTempFile) else {
return nil
}
for i in 0 ..< higherResolutionItem.numFrames {
guard let duration = higherResolutionItem.getFrameDuration(index: i) else {
break
}
writer.add(with: { yuva in
guard let frame = higherResolutionItem.getFrame(index: i, requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else {
return
}
switch frame.format {
case .rgba:
return
case let .yuva(y, u, v, a):
yuva.yPlane.copyScaled(fromPlane: y)
yuva.uPlane.copyScaled(fromPlane: u)
yuva.vPlane.copyScaled(fromPlane: v)
yuva.aPlane.copyScaled(fromPlane: a)
}
}, proposedWidth: width, proposedHeight: height, duration: duration)
}
guard let result = writer.finish() else {
return nil
}
guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else {
return nil
}
let _ = try? FileManager.default.removeItem(atPath: itemPath)
guard let _ = try? FileManager.default.moveItem(atPath: result.path, toPath: itemPath) else {
return nil
}
guard let item = loadItem(path: itemPath) else {
return nil
}
return item
}
private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, baseName: String, baseSuffix: String, width: Int, height: Int) -> String? {
var candidates: [(path: String, width: Int, height: Int)] = []
if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: itemDirectoryPath), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants, errorHandler: nil) {
for url in enumerator {
guard let url = url as? URL else {
continue
}
let fileName = url.lastPathComponent
if fileName.hasPrefix(baseName) {
let scanner = Scanner(string: fileName)
guard scanner.scanString(baseName, into: nil) else {
continue
}
var itemWidth: Int = 0
guard scanner.scanInt(&itemWidth) else {
continue
}
guard scanner.scanString("x", into: nil) else {
continue
}
var itemHeight: Int = 0
guard scanner.scanInt(&itemHeight) else {
continue
}
if !baseSuffix.isEmpty {
guard scanner.scanString(baseSuffix, into: nil) else {
continue
}
}
guard scanner.isAtEnd else {
continue
}
if itemWidth > width && itemHeight > height {
candidates.append((url.path, itemWidth, itemHeight))
}
}
}
}
if !candidates.isEmpty {
candidates.sort(by: { $0.width < $1.width })
return candidates[0].path
}
return nil
}
public final class AnimationCacheImpl: AnimationCache {
private final class Impl {
private final class ItemContext {
@@ -789,7 +1101,7 @@ public final class AnimationCacheImpl: AnimationCache {
}
func get(sourceId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable {
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))"))
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height))
let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)"
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
@@ -878,43 +1190,58 @@ public final class AnimationCacheImpl: AnimationCache {
}
}
static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize) -> AnimationCacheItem? {
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))"))
static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? {
let hashString = md5Hash(sourceId)
let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height))
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
if FileManager.default.fileExists(atPath: itemFirstFramePath) {
return loadItem(path: itemFirstFramePath)
} else {
return nil
}
if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) {
if let adaptedItem = adaptItemFromHigherResolution(itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile) {
return adaptedItem
}
}
return nil
}
static func getFirstFrame(basePath: String, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))"))
static func getFirstFrame(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
let hashString = md5Hash(sourceId)
let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height))
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = loadItem(path: itemFirstFramePath) {
completion(item)
return EmptyDisposable
} else {
completion(nil)
return EmptyDisposable
}
if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) {
if let adaptedItem = adaptItemFromHigherResolution(itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile) {
completion(adaptedItem)
return EmptyDisposable
}
}
completion(nil)
return EmptyDisposable
}
}
private let queue: Queue
private let basePath: String
private let impl: QueueLocalObject<Impl>
private let allocateTempFile: () -> String
public init(basePath: String, allocateTempFile: @escaping () -> String) {
let queue = Queue()
self.queue = queue
self.basePath = basePath
self.allocateTempFile = allocateTempFile
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile)
})
@@ -939,15 +1266,16 @@ public final class AnimationCacheImpl: AnimationCache {
}
public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? {
return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size)
return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile)
}
public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
let disposable = MetaDisposable()
let basePath = self.basePath
let allocateTempFile = self.allocateTempFile
queue.async {
disposable.set(Impl.getFirstFrame(basePath: basePath, sourceId: sourceId, size: size, completion: completion))
disposable.set(Impl.getFirstFrame(basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, completion: completion))
}
return disposable

View File

@@ -2,27 +2,46 @@ import Foundation
import UIKit
import ImageDCT
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
final class ImagePlane {
let width: Int
let height: Int
let bytesPerRow: Int
let rowAlignment: Int
let components: Int
var data: Data
init(width: Int, height: Int, components: Int, bytesPerRow: Int?) {
init(width: Int, height: Int, components: Int, rowAlignment: Int?) {
self.width = width
self.height = height
self.bytesPerRow = bytesPerRow ?? (width * components)
self.rowAlignment = rowAlignment ?? 1
self.bytesPerRow = alignUp(size: width * components, align: self.rowAlignment)
self.components = components
self.data = Data(count: self.bytesPerRow * height)
}
}
extension ImagePlane {
func copyScaled(fromPlane plane: AnimationCacheItemFrame.Plane) {
self.data.withUnsafeMutableBytes { destBytes in
plane.data.withUnsafeBytes { srcBytes in
scaleImagePlane(destBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(self.width), Int32(self.height), Int32(self.bytesPerRow), srcBytes.baseAddress!.assumingMemoryBound(to: UInt8.self), Int32(plane.width), Int32(plane.height), Int32(plane.bytesPerRow))
}
}
}
}
final class ImageARGB {
let argbPlane: ImagePlane
init(width: Int, height: Int, bytesPerRow: Int?) {
self.argbPlane = ImagePlane(width: width, height: height, components: 4, bytesPerRow: bytesPerRow)
init(width: Int, height: Int, rowAlignment: Int?) {
self.argbPlane = ImagePlane(width: width, height: height, components: 4, rowAlignment: rowAlignment)
}
}
@@ -32,11 +51,11 @@ final class ImageYUVA420 {
let vPlane: ImagePlane
let aPlane: ImagePlane
init(width: Int, height: Int, bytesPerRow: Int?) {
self.yPlane = ImagePlane(width: width, height: height, components: 1, bytesPerRow: bytesPerRow)
self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, bytesPerRow: bytesPerRow)
self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, bytesPerRow: bytesPerRow)
self.aPlane = ImagePlane(width: width, height: height, components: 1, bytesPerRow: bytesPerRow)
init(width: Int, height: Int, rowAlignment: Int?) {
self.yPlane = ImagePlane(width: width, height: height, components: 1, rowAlignment: rowAlignment)
self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, rowAlignment: rowAlignment)
self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1, rowAlignment: rowAlignment)
self.aPlane = ImagePlane(width: width, height: height, components: 1, rowAlignment: rowAlignment)
}
}
@@ -92,8 +111,8 @@ extension ImageARGB {
}
}
func toYUVA420(bytesPerRow: Int?) -> ImageYUVA420 {
let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height, bytesPerRow: bytesPerRow)
func toYUVA420(rowAlignment: Int?) -> ImageYUVA420 {
let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height, rowAlignment: rowAlignment)
self.toYUVA420(target: resultImage)
return resultImage
}
@@ -125,8 +144,8 @@ extension ImageYUVA420 {
}
}
func toARGB(bytesPerRow: Int?) -> ImageARGB {
let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height, bytesPerRow: bytesPerRow)
func toARGB(rowAlignment: Int?) -> ImageARGB {
let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height, rowAlignment: rowAlignment)
self.toARGB(target: resultImage)
return resultImage
}
@@ -221,8 +240,8 @@ extension DctCoefficientsYUVA420 {
}
}
func idct(dctData: DctData, bytesPerRow: Int?) -> ImageYUVA420 {
let resultImage = ImageYUVA420(width: self.yPlane.width, height: self.yPlane.height, bytesPerRow: bytesPerRow)
func idct(dctData: DctData, rowAlignment: Int?) -> ImageYUVA420 {
let resultImage = ImageYUVA420(width: self.yPlane.width, height: self.yPlane.height, rowAlignment: rowAlignment)
self.idct(dctData: dctData, target: resultImage)
return resultImage
}