mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Entity input: improved animation cache and rendering
This commit is contained in:
@@ -3,6 +3,7 @@ import UIKit
|
||||
import SwiftSignalKit
|
||||
import CryptoUtils
|
||||
import ManagedFile
|
||||
import Compression
|
||||
|
||||
public final class AnimationCacheItemFrame {
|
||||
public enum Format {
|
||||
@@ -25,19 +26,51 @@ public final class AnimationCacheItemFrame {
|
||||
public final class AnimationCacheItem {
|
||||
public let numFrames: Int
|
||||
private let getFrameImpl: (Int) -> AnimationCacheItemFrame?
|
||||
private let getFrameIndexImpl: (Double) -> Int
|
||||
|
||||
public init(numFrames: Int, getFrame: @escaping (Int) -> AnimationCacheItemFrame?) {
|
||||
public init(numFrames: Int, getFrame: @escaping (Int) -> AnimationCacheItemFrame?, getFrameIndexImpl: @escaping (Double) -> Int) {
|
||||
self.numFrames = numFrames
|
||||
self.getFrameImpl = getFrame
|
||||
self.getFrameIndexImpl = getFrameIndexImpl
|
||||
}
|
||||
|
||||
public func getFrame(index: Int) -> AnimationCacheItemFrame? {
|
||||
return self.getFrameImpl(index)
|
||||
}
|
||||
|
||||
public func getFrame(at duration: Double) -> AnimationCacheItemFrame? {
|
||||
let index = self.getFrameIndexImpl(duration)
|
||||
return self.getFrameImpl(index)
|
||||
}
|
||||
}
|
||||
|
||||
public struct AnimationCacheItemDrawingSurface {
|
||||
public let argb: UnsafeMutablePointer<UInt8>
|
||||
public let width: Int
|
||||
public let height: Int
|
||||
public let bytesPerRow: Int
|
||||
public let length: Int
|
||||
|
||||
init(
|
||||
argb: UnsafeMutablePointer<UInt8>,
|
||||
width: Int,
|
||||
height: Int,
|
||||
bytesPerRow: Int,
|
||||
length: Int
|
||||
) {
|
||||
self.argb = argb
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bytesPerRow = bytesPerRow
|
||||
self.length = length
|
||||
}
|
||||
}
|
||||
|
||||
public protocol AnimationCacheItemWriter: AnyObject {
|
||||
func add(bytes: UnsafeRawPointer, length: Int, width: Int, height: Int, bytesPerRow: Int, duration: Double)
|
||||
var queue: Queue { get }
|
||||
var isCancelled: Bool { get }
|
||||
|
||||
func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double)
|
||||
func finish()
|
||||
}
|
||||
|
||||
@@ -53,7 +86,8 @@ public final class AnimationCacheItemResult {
|
||||
|
||||
public protocol AnimationCache: AnyObject {
|
||||
func get(sourceId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Signal<AnimationCacheItemResult, NoError>
|
||||
func getSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem?
|
||||
func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem?
|
||||
func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable
|
||||
}
|
||||
|
||||
private func md5Hash(_ string: String) -> String {
|
||||
@@ -80,11 +114,82 @@ private func itemSubpath(hashString: String) -> (directory: String, fileName: St
|
||||
return (directory, hashString)
|
||||
}
|
||||
|
||||
private func roundUp(_ numToRound: Int, multiple: Int) -> Int {
|
||||
if multiple == 0 {
|
||||
return numToRound
|
||||
}
|
||||
|
||||
let remainder = numToRound % multiple
|
||||
if remainder == 0 {
|
||||
return numToRound;
|
||||
}
|
||||
|
||||
return numToRound + multiple - remainder
|
||||
}
|
||||
|
||||
private func compressData(data: Data, addSizeHeader: Bool = false) -> Data? {
|
||||
let algorithm: compression_algorithm = COMPRESSION_LZFSE
|
||||
|
||||
let scratchData = malloc(compression_encode_scratch_buffer_size(algorithm))!
|
||||
defer {
|
||||
free(scratchData)
|
||||
}
|
||||
|
||||
let headerSize = addSizeHeader ? 4 : 0
|
||||
var compressedData = Data(count: headerSize + data.count + 16 * 1024)
|
||||
let resultSize = compressedData.withUnsafeMutableBytes { buffer -> Int in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return 0
|
||||
}
|
||||
|
||||
if addSizeHeader {
|
||||
var decompressedSize: UInt32 = UInt32(data.count)
|
||||
memcpy(bytes, &decompressedSize, 4)
|
||||
}
|
||||
|
||||
return data.withUnsafeBytes { sourceBuffer -> Int in
|
||||
return compression_encode_buffer(bytes.advanced(by: headerSize), buffer.count - headerSize, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), sourceBuffer.count, scratchData, algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
if resultSize <= 0 {
|
||||
return nil
|
||||
}
|
||||
compressedData.count = headerSize + resultSize
|
||||
return compressedData
|
||||
}
|
||||
|
||||
private func decompressData(data: Data, range: Range<Int>, decompressedSize: Int) -> Data? {
|
||||
let algorithm: compression_algorithm = COMPRESSION_LZFSE
|
||||
|
||||
let scratchData = malloc(compression_decode_scratch_buffer_size(algorithm))!
|
||||
defer {
|
||||
free(scratchData)
|
||||
}
|
||||
|
||||
var decompressedFrameData = Data(count: decompressedSize)
|
||||
let resultSize = decompressedFrameData.withUnsafeMutableBytes { buffer -> Int in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return 0
|
||||
}
|
||||
return data.withUnsafeBytes { sourceBuffer -> Int in
|
||||
return compression_decode_buffer(bytes, buffer.count, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: range.lowerBound), range.upperBound - range.lowerBound, scratchData, algorithm)
|
||||
}
|
||||
}
|
||||
|
||||
if resultSize <= 0 {
|
||||
return nil
|
||||
}
|
||||
if decompressedFrameData.count != resultSize {
|
||||
decompressedFrameData.count = resultSize
|
||||
}
|
||||
return decompressedFrameData
|
||||
}
|
||||
|
||||
private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
||||
private struct ParameterSet: Equatable {
|
||||
var width: Int
|
||||
var height: Int
|
||||
var bytesPerRow: Int
|
||||
struct CompressedResult {
|
||||
var animationPath: String
|
||||
var firstFramePath: String
|
||||
}
|
||||
|
||||
private struct FrameMetadata {
|
||||
@@ -93,10 +198,19 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
||||
var duration: Double
|
||||
}
|
||||
|
||||
private let file: ManagedFile
|
||||
private let completion: (Bool) -> Void
|
||||
let queue: Queue
|
||||
var isCancelled: Bool = false
|
||||
|
||||
private var currentParameterSet: ParameterSet?
|
||||
private let decompressedPath: String
|
||||
private let compressedPath: String
|
||||
private let firstFramePath: String
|
||||
private var file: ManagedFile?
|
||||
private let completion: (CompressedResult?) -> Void
|
||||
|
||||
private var currentSurface: ImageARGB?
|
||||
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
|
||||
@@ -104,44 +218,141 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
||||
private var frames: [FrameMetadata] = []
|
||||
private var contentLength: Int = 0
|
||||
|
||||
private let dctQuality: Int
|
||||
|
||||
private let lock = Lock()
|
||||
|
||||
init?(tempPath: String, completion: @escaping (Bool) -> Void) {
|
||||
guard let file = ManagedFile(queue: nil, path: tempPath, mode: .readwrite) else {
|
||||
init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) {
|
||||
self.dctQuality = 67
|
||||
|
||||
self.queue = queue
|
||||
self.decompressedPath = allocateTempFile()
|
||||
self.compressedPath = allocateTempFile()
|
||||
self.firstFramePath = allocateTempFile()
|
||||
|
||||
guard let file = ManagedFile(queue: nil, path: self.decompressedPath, mode: .readwrite) else {
|
||||
return nil
|
||||
}
|
||||
self.file = file
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func add(bytes: UnsafeRawPointer, length: Int, width: Int, height: Int, bytesPerRow: Int, duration: Double) {
|
||||
func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) {
|
||||
if self.isFailed || self.isFinished {
|
||||
return
|
||||
}
|
||||
|
||||
self.lock.locked {
|
||||
if self.isFailed {
|
||||
guard !self.isFailed, !self.isFinished, let file = self.file else {
|
||||
return
|
||||
}
|
||||
|
||||
let parameterSet = ParameterSet(width: width, height: height, bytesPerRow: bytesPerRow)
|
||||
if let currentParameterSet = self.currentParameterSet {
|
||||
if currentParameterSet != parameterSet {
|
||||
let width = roundUp(proposedWidth, multiple: 16)
|
||||
let height = roundUp(proposedWidth, multiple: 16)
|
||||
|
||||
var isFirstFrame = false
|
||||
|
||||
let surface: ImageARGB
|
||||
if let current = self.currentSurface {
|
||||
if current.argbPlane.width == width && current.argbPlane.height == height {
|
||||
surface = current
|
||||
} else {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
} else {
|
||||
self.currentParameterSet = parameterSet
|
||||
isFirstFrame = true
|
||||
|
||||
self.file.write(1 as UInt32)
|
||||
|
||||
self.file.write(UInt32(parameterSet.width))
|
||||
self.file.write(UInt32(parameterSet.height))
|
||||
self.file.write(UInt32(parameterSet.bytesPerRow))
|
||||
|
||||
self.contentLengthOffset = Int(self.file.position())
|
||||
self.file.write(0 as UInt32)
|
||||
surface = ImageARGB(width: width, height: height)
|
||||
self.currentSurface = surface
|
||||
}
|
||||
|
||||
self.frames.append(FrameMetadata(offset: Int(self.file.position()), length: length, duration: duration))
|
||||
let _ = self.file.write(bytes, count: length)
|
||||
self.contentLength += length
|
||||
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 {
|
||||
yuvaSurface = ImageYUVA420(width: width, height: height)
|
||||
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
|
||||
}
|
||||
|
||||
surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Void in
|
||||
drawingBlock(AnimationCacheItemDrawingSurface(
|
||||
argb: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
width: width,
|
||||
height: height,
|
||||
bytesPerRow: surface.argbPlane.bytesPerRow,
|
||||
length: bytes.count
|
||||
))
|
||||
}
|
||||
|
||||
surface.toYUVA420(target: 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,27 +363,96 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
||||
self.isFinished = true
|
||||
shouldComplete = true
|
||||
|
||||
guard let contentLengthOffset = self.contentLengthOffset else {
|
||||
guard let contentLengthOffset = self.contentLengthOffset, let file = self.file else {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
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, let dctCoefficients = self.currentDctCoefficients, let dctData = self.currentDctData {
|
||||
var firstFrameData = Data(capacity: 4 * 5 + self.frames[0].length)
|
||||
|
||||
writeUInt32(data: &firstFrameData, value: 2 as UInt32)
|
||||
writeUInt32(data: &firstFrameData, value: UInt32(dctCoefficients.yPlane.width))
|
||||
writeUInt32(data: &firstFrameData, value: UInt32(dctCoefficients.yPlane.height))
|
||||
writeUInt32(data: &firstFrameData, value: UInt32(dctData.quality))
|
||||
|
||||
writeUInt32(data: &firstFrameData, value: UInt32(self.frames[0].length))
|
||||
let firstFrameStart = 4 * 5
|
||||
|
||||
file.seek(position: Int64(self.frames[0].offset))
|
||||
firstFrameData.count += self.frames[0].length
|
||||
firstFrameData.withUnsafeMutableBytes { bytes in
|
||||
let _ = file.read(bytes.baseAddress!.advanced(by: 4 * 5), self.frames[0].length)
|
||||
}
|
||||
|
||||
writeUInt32(data: &firstFrameData, value: UInt32(1))
|
||||
writeUInt32(data: &firstFrameData, value: UInt32(firstFrameStart))
|
||||
writeUInt32(data: &firstFrameData, value: UInt32(self.frames[0].length))
|
||||
writeFloat32(data: &firstFrameData, value: Float32(1.0))
|
||||
|
||||
guard let compressedFirstFrameData = compressData(data: firstFrameData, addSizeHeader: true) else {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
guard let _ = try? compressedFirstFrameData.write(to: URL(fileURLWithPath: self.firstFramePath)) else {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
} else {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
|
||||
let metadataPosition = self.file.position()
|
||||
self.file.seek(position: Int64(contentLengthOffset))
|
||||
self.file.write(UInt32(self.contentLength))
|
||||
|
||||
self.file.seek(position: metadataPosition)
|
||||
self.file.write(UInt32(self.frames.count))
|
||||
for frame in self.frames {
|
||||
self.file.write(UInt32(frame.offset))
|
||||
self.file.write(UInt32(frame.length))
|
||||
self.file.write(Float32(frame.duration))
|
||||
if !self.isFailed {
|
||||
self.file = nil
|
||||
|
||||
file._unsafeClose()
|
||||
|
||||
guard let uncompressedData = try? Data(contentsOf: URL(fileURLWithPath: self.decompressedPath), options: .alwaysMapped) else {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
guard let compressedData = compressData(data: uncompressedData) else {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
guard let compressedFile = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else {
|
||||
self.isFailed = true
|
||||
return
|
||||
}
|
||||
compressedFile.write(Int32(uncompressedData.count))
|
||||
let _ = compressedFile.write(compressedData)
|
||||
compressedFile._unsafeClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shouldComplete {
|
||||
self.completion(!self.isFailed)
|
||||
let _ = try? FileManager.default.removeItem(atPath: self.decompressedPath)
|
||||
|
||||
if !self.isFailed {
|
||||
self.completion(CompressedResult(
|
||||
animationPath: self.compressedPath,
|
||||
firstFramePath: self.firstFramePath
|
||||
))
|
||||
} else {
|
||||
let _ = try? FileManager.default.removeItem(atPath: self.compressedPath)
|
||||
let _ = try? FileManager.default.removeItem(atPath: self.firstFramePath)
|
||||
self.completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,12 +465,34 @@ private final class AnimationCacheItemAccessor {
|
||||
|
||||
private let data: Data
|
||||
private let frameMapping: [Int: FrameInfo]
|
||||
private let format: AnimationCacheItemFrame.Format
|
||||
private let durationMapping: [Double]
|
||||
private let totalDuration: Double
|
||||
|
||||
init(data: Data, frameMapping: [Int: FrameInfo], format: AnimationCacheItemFrame.Format) {
|
||||
private var currentYUVASurface: ImageYUVA420
|
||||
private var currentDctData: DctData
|
||||
private var currentDctCoefficients: DctCoefficientsYUVA420
|
||||
|
||||
init(data: Data, frameMapping: [FrameInfo], width: Int, height: Int, dctQuality: Int) {
|
||||
self.data = data
|
||||
self.frameMapping = frameMapping
|
||||
self.format = format
|
||||
|
||||
var resultFrameMapping: [Int: FrameInfo] = [:]
|
||||
var durationMapping: [Double] = []
|
||||
var totalDuration: Double = 0.0
|
||||
|
||||
for i in 0 ..< frameMapping.count {
|
||||
let frame = frameMapping[i]
|
||||
resultFrameMapping[i] = frame
|
||||
totalDuration += frame.duration
|
||||
durationMapping.append(totalDuration)
|
||||
}
|
||||
|
||||
self.frameMapping = resultFrameMapping
|
||||
self.durationMapping = durationMapping
|
||||
self.totalDuration = totalDuration
|
||||
|
||||
self.currentYUVASurface = ImageYUVA420(width: width, height: height)
|
||||
self.currentDctData = DctData(quality: dctQuality)
|
||||
self.currentDctCoefficients = DctCoefficientsYUVA420(width: width, height: height)
|
||||
}
|
||||
|
||||
func getFrame(index: Int) -> AnimationCacheItemFrame? {
|
||||
@@ -198,7 +500,56 @@ private final class AnimationCacheItemAccessor {
|
||||
return nil
|
||||
}
|
||||
|
||||
return AnimationCacheItemFrame(data: data, range: frameInfo.range, format: self.format, duration: frameInfo.duration)
|
||||
let currentSurface = ImageARGB(width: self.currentYUVASurface.yPlane.width, height: self.currentYUVASurface.yPlane.height)
|
||||
|
||||
var frameDataOffset = 0
|
||||
let frameLength = frameInfo.range.upperBound - frameInfo.range.lowerBound
|
||||
for i in 0 ..< 4 {
|
||||
let dctPlane: DctCoefficientPlane
|
||||
switch i {
|
||||
case 0:
|
||||
dctPlane = self.currentDctCoefficients.yPlane
|
||||
case 1:
|
||||
dctPlane = self.currentDctCoefficients.uPlane
|
||||
case 2:
|
||||
dctPlane = self.currentDctCoefficients.vPlane
|
||||
case 3:
|
||||
dctPlane = self.currentDctCoefficients.aPlane
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
if frameDataOffset + dctPlane.data.count > frameLength {
|
||||
break
|
||||
}
|
||||
|
||||
dctPlane.data.withUnsafeMutableBytes { targetBuffer -> Void in
|
||||
self.data.copyBytes(to: targetBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (frameInfo.range.lowerBound + frameDataOffset) ..< (frameInfo.range.lowerBound + frameDataOffset + targetBuffer.count))
|
||||
}
|
||||
|
||||
frameDataOffset += dctPlane.data.count
|
||||
}
|
||||
|
||||
self.currentDctCoefficients.idct(dctData: self.currentDctData, target: self.currentYUVASurface)
|
||||
self.currentYUVASurface.toARGB(target: currentSurface)
|
||||
|
||||
return AnimationCacheItemFrame(data: currentSurface.argbPlane.data, range: 0 ..< currentSurface.argbPlane.data.count, format: .rgba(width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: frameInfo.duration)
|
||||
}
|
||||
|
||||
func getFrameIndex(duration: Double) -> Int {
|
||||
if self.totalDuration == 0.0 {
|
||||
return 0
|
||||
}
|
||||
if self.durationMapping.count <= 1 {
|
||||
return 0
|
||||
}
|
||||
let normalizedDuration = duration.truncatingRemainder(dividingBy: self.totalDuration)
|
||||
for i in 1 ..< self.durationMapping.count {
|
||||
if normalizedDuration < self.durationMapping[i] {
|
||||
return i - 1
|
||||
}
|
||||
}
|
||||
return self.durationMapping.count - 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,10 +564,54 @@ private func readUInt32(data: Data, offset: Int) -> UInt32 {
|
||||
return value
|
||||
}
|
||||
|
||||
private func readFloat32(data: Data, offset: Int) -> Float32 {
|
||||
var value: Float32 = 0
|
||||
withUnsafeMutableBytes(of: &value, { bytes -> Void in
|
||||
data.withUnsafeBytes { dataBytes -> Void in
|
||||
memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4)
|
||||
}
|
||||
})
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private func writeUInt32(data: inout Data, value: UInt32) {
|
||||
var value: UInt32 = value
|
||||
withUnsafeBytes(of: &value, { bytes -> Void in
|
||||
data.count += 4
|
||||
data.withUnsafeMutableBytes { dataBytes -> Void in
|
||||
memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func writeFloat32(data: inout Data, value: Float32) {
|
||||
var value: Float32 = value
|
||||
withUnsafeBytes(of: &value, { bytes -> Void in
|
||||
data.count += 4
|
||||
data.withUnsafeMutableBytes { dataBytes -> Void in
|
||||
memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func loadItem(path: String) -> AnimationCacheItem? {
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else {
|
||||
guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if compressedData.count < 4 {
|
||||
return nil
|
||||
}
|
||||
let decompressedSize = readUInt32(data: compressedData, offset: 0)
|
||||
|
||||
if decompressedSize <= 0 || decompressedSize > 20 * 1024 * 1024 {
|
||||
return nil
|
||||
}
|
||||
guard let data = decompressData(data: compressedData, range: 4 ..< compressedData.count, decompressedSize: Int(decompressedSize)) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let dataLength = data.count
|
||||
|
||||
var offset = 0
|
||||
@@ -226,7 +621,7 @@ private func loadItem(path: String) -> AnimationCacheItem? {
|
||||
}
|
||||
let formatVersion = readUInt32(data: data, offset: offset)
|
||||
offset += 4
|
||||
if formatVersion != 1 {
|
||||
if formatVersion != 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -245,7 +640,7 @@ private func loadItem(path: String) -> AnimationCacheItem? {
|
||||
guard dataLength >= offset + 4 else {
|
||||
return nil
|
||||
}
|
||||
let bytesPerRow = readUInt32(data: data, offset: offset)
|
||||
let dctQuality = readUInt32(data: data, offset: offset)
|
||||
offset += 4
|
||||
|
||||
guard dataLength >= offset + 4 else {
|
||||
@@ -262,8 +657,8 @@ private func loadItem(path: String) -> AnimationCacheItem? {
|
||||
let numFrames = readUInt32(data: data, offset: offset)
|
||||
offset += 4
|
||||
|
||||
var frameMapping: [Int: AnimationCacheItemAccessor.FrameInfo] = [:]
|
||||
for i in 0 ..< Int(numFrames) {
|
||||
var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = []
|
||||
for _ in 0 ..< Int(numFrames) {
|
||||
guard dataLength >= offset + 4 + 4 + 4 else {
|
||||
return nil
|
||||
}
|
||||
@@ -272,16 +667,18 @@ private func loadItem(path: String) -> AnimationCacheItem? {
|
||||
offset += 4
|
||||
let frameLength = readUInt32(data: data, offset: offset)
|
||||
offset += 4
|
||||
let frameDuration = readUInt32(data: data, offset: offset)
|
||||
let frameDuration = readFloat32(data: data, offset: offset)
|
||||
offset += 4
|
||||
|
||||
frameMapping[i] = AnimationCacheItemAccessor.FrameInfo(range: Int(frameStart) ..< Int(frameStart + frameLength), duration: Double(frameDuration))
|
||||
frameMapping.append(AnimationCacheItemAccessor.FrameInfo(range: Int(frameStart) ..< Int(frameStart + frameLength), duration: Double(frameDuration)))
|
||||
}
|
||||
|
||||
let itemAccessor = AnimationCacheItemAccessor(data: data, frameMapping: frameMapping, format: .rgba(width: Int(width), height: Int(height), bytesPerRow: Int(bytesPerRow)))
|
||||
let itemAccessor = AnimationCacheItemAccessor(data: data, frameMapping: frameMapping, width: Int(width), height: Int(height), dctQuality: Int(dctQuality))
|
||||
|
||||
return AnimationCacheItem(numFrames: Int(numFrames), getFrame: { index in
|
||||
return itemAccessor.getFrame(index: index)
|
||||
}, getFrameIndexImpl: { duration in
|
||||
return itemAccessor.getFrameIndex(duration: duration)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -300,10 +697,14 @@ public final class AnimationCacheImpl: AnimationCache {
|
||||
private let basePath: String
|
||||
private let allocateTempFile: () -> String
|
||||
|
||||
private let fetchQueues: [Queue]
|
||||
private var nextFetchQueueIndex: Int = 0
|
||||
|
||||
private var itemContexts: [String: ItemContext] = [:]
|
||||
|
||||
init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String) {
|
||||
self.queue = queue
|
||||
self.fetchQueues = (0 ..< 2).map { _ in Queue() }
|
||||
self.basePath = basePath
|
||||
self.allocateTempFile = allocateTempFile
|
||||
}
|
||||
@@ -315,9 +716,10 @@ public final class AnimationCacheImpl: AnimationCache {
|
||||
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))"))
|
||||
let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
|
||||
let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)"
|
||||
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
|
||||
|
||||
if FileManager.default.fileExists(atPath: itemPath) {
|
||||
updateResult(AnimationCacheItemResult(item: loadItem(path: itemPath), isFinal: true))
|
||||
if FileManager.default.fileExists(atPath: itemPath), let item = loadItem(path: itemPath) {
|
||||
updateResult(AnimationCacheItemResult(item: item, isFinal: true))
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
@@ -338,8 +740,7 @@ public final class AnimationCacheImpl: AnimationCache {
|
||||
updateResult(AnimationCacheItemResult(item: nil, isFinal: false))
|
||||
|
||||
if beginFetch {
|
||||
let tempPath = self.allocateTempFile()
|
||||
guard let writer = AnimationCacheItemWriterImpl(tempPath: tempPath, completion: { [weak self, weak itemContext] success in
|
||||
guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[self.nextFetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in
|
||||
queue.async {
|
||||
guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[sourceId] else {
|
||||
return
|
||||
@@ -347,13 +748,18 @@ public final class AnimationCacheImpl: AnimationCache {
|
||||
|
||||
strongSelf.itemContexts.removeValue(forKey: sourceId)
|
||||
|
||||
guard success else {
|
||||
guard let result = result else {
|
||||
return
|
||||
}
|
||||
guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else {
|
||||
return
|
||||
}
|
||||
guard let _ = try? FileManager.default.moveItem(atPath: tempPath, toPath: itemPath) else {
|
||||
let _ = try? FileManager.default.removeItem(atPath: itemPath)
|
||||
guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else {
|
||||
return
|
||||
}
|
||||
let _ = try? FileManager.default.removeItem(atPath: itemFirstFramePath)
|
||||
guard let _ = try? FileManager.default.moveItem(atPath: result.firstFramePath, toPath: itemFirstFramePath) else {
|
||||
return
|
||||
}
|
||||
guard let item = loadItem(path: itemPath) else {
|
||||
@@ -368,9 +774,14 @@ public final class AnimationCacheImpl: AnimationCache {
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
let fetchDisposable = fetch(size, writer)
|
||||
let fetchDisposable = MetaDisposable()
|
||||
fetchDisposable.set(fetch(size, writer))
|
||||
|
||||
itemContext.disposable.set(ActionDisposable {
|
||||
itemContext.disposable.set(ActionDisposable { [weak writer] in
|
||||
if let writer = writer {
|
||||
writer.isCancelled = true
|
||||
}
|
||||
|
||||
fetchDisposable.dispose()
|
||||
})
|
||||
}
|
||||
@@ -389,25 +800,43 @@ public final class AnimationCacheImpl: AnimationCache {
|
||||
}
|
||||
}
|
||||
|
||||
func getSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? {
|
||||
static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize) -> AnimationCacheItem? {
|
||||
let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))"))
|
||||
let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)"
|
||||
let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)"
|
||||
let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)"
|
||||
let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f"
|
||||
|
||||
if FileManager.default.fileExists(atPath: itemPath) {
|
||||
return loadItem(path: itemPath)
|
||||
if FileManager.default.fileExists(atPath: itemFirstFramePath) {
|
||||
return loadItem(path: itemFirstFramePath)
|
||||
} else {
|
||||
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))"))
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let queue: Queue
|
||||
private let basePath: String
|
||||
private let impl: QueueLocalObject<Impl>
|
||||
|
||||
public init(basePath: String, allocateTempFile: @escaping () -> String) {
|
||||
let queue = Queue()
|
||||
self.queue = queue
|
||||
self.basePath = basePath
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile)
|
||||
})
|
||||
@@ -431,9 +860,18 @@ public final class AnimationCacheImpl: AnimationCache {
|
||||
|> runOn(self.queue)
|
||||
}
|
||||
|
||||
public func getSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? {
|
||||
return self.impl.syncWith { impl -> AnimationCacheItem? in
|
||||
return impl.getSynchronously(sourceId: sourceId, size: size)
|
||||
public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? {
|
||||
return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size)
|
||||
}
|
||||
|
||||
public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable {
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
let basePath = self.basePath
|
||||
queue.async {
|
||||
disposable.set(Impl.getFirstFrame(basePath: basePath, sourceId: sourceId, size: size, completion: completion))
|
||||
}
|
||||
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import DCT
|
||||
|
||||
final class ImagePlane {
|
||||
let width: Int
|
||||
let height: Int
|
||||
let bytesPerRow: Int
|
||||
let components: Int
|
||||
var data: Data
|
||||
|
||||
init(width: Int, height: Int, components: Int) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.bytesPerRow = width * components
|
||||
self.components = components
|
||||
self.data = Data(count: width * components * height)
|
||||
}
|
||||
}
|
||||
|
||||
final class ImageARGB {
|
||||
let argbPlane: ImagePlane
|
||||
|
||||
init(width: Int, height: Int) {
|
||||
self.argbPlane = ImagePlane(width: width, height: height, components: 4)
|
||||
}
|
||||
}
|
||||
|
||||
final class ImageYUVA420 {
|
||||
let yPlane: ImagePlane
|
||||
let uPlane: ImagePlane
|
||||
let vPlane: ImagePlane
|
||||
let aPlane: ImagePlane
|
||||
|
||||
init(width: Int, height: Int) {
|
||||
self.yPlane = ImagePlane(width: width, height: height, components: 1)
|
||||
self.uPlane = ImagePlane(width: width / 2, height: height / 2, components: 1)
|
||||
self.vPlane = ImagePlane(width: width / 2, height: height / 2, components: 1)
|
||||
self.aPlane = ImagePlane(width: width, height: height, components: 1)
|
||||
}
|
||||
}
|
||||
|
||||
final class DctCoefficientPlane {
|
||||
let width: Int
|
||||
let height: Int
|
||||
var data: Data
|
||||
|
||||
init(width: Int, height: Int) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.data = Data(count: width * 2 * height)
|
||||
}
|
||||
}
|
||||
|
||||
final class DctCoefficientsYUVA420 {
|
||||
let yPlane: DctCoefficientPlane
|
||||
let uPlane: DctCoefficientPlane
|
||||
let vPlane: DctCoefficientPlane
|
||||
let aPlane: DctCoefficientPlane
|
||||
|
||||
init(width: Int, height: Int) {
|
||||
self.yPlane = DctCoefficientPlane(width: width, height: height)
|
||||
self.uPlane = DctCoefficientPlane(width: width / 2, height: height / 2)
|
||||
self.vPlane = DctCoefficientPlane(width: width / 2, height: height / 2)
|
||||
self.aPlane = DctCoefficientPlane(width: width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageARGB {
|
||||
func toYUVA420(target: ImageYUVA420) {
|
||||
precondition(self.argbPlane.width == target.yPlane.width && self.argbPlane.height == target.yPlane.height)
|
||||
|
||||
self.argbPlane.data.withUnsafeBytes { argbBuffer -> Void in
|
||||
target.yPlane.data.withUnsafeMutableBytes { yBuffer -> Void in
|
||||
target.uPlane.data.withUnsafeMutableBytes { uBuffer -> Void in
|
||||
target.vPlane.data.withUnsafeMutableBytes { vBuffer -> Void in
|
||||
target.aPlane.data.withUnsafeMutableBytes { aBuffer -> Void in
|
||||
splitRGBAIntoYUVAPlanes(
|
||||
argbBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
yBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
uBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
vBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
aBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
Int32(self.argbPlane.width),
|
||||
Int32(self.argbPlane.height),
|
||||
Int32(self.argbPlane.bytesPerRow)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toYUVA420() -> ImageYUVA420 {
|
||||
let resultImage = ImageYUVA420(width: self.argbPlane.width, height: self.argbPlane.height)
|
||||
self.toYUVA420(target: resultImage)
|
||||
return resultImage
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageYUVA420 {
|
||||
func toARGB(target: ImageARGB) {
|
||||
precondition(self.yPlane.width == target.argbPlane.width && self.yPlane.height == target.argbPlane.height)
|
||||
|
||||
self.yPlane.data.withUnsafeBytes { yBuffer -> Void in
|
||||
self.uPlane.data.withUnsafeBytes { uBuffer -> Void in
|
||||
self.vPlane.data.withUnsafeBytes { vBuffer -> Void in
|
||||
self.aPlane.data.withUnsafeBytes { aBuffer -> Void in
|
||||
target.argbPlane.data.withUnsafeMutableBytes { argbBuffer -> Void in
|
||||
combineYUVAPlanesIntoARBB(
|
||||
argbBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
yBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
uBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
vBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
aBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self),
|
||||
Int32(target.argbPlane.width),
|
||||
Int32(target.argbPlane.height),
|
||||
Int32(target.argbPlane.bytesPerRow)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toARGB() -> ImageARGB {
|
||||
let resultImage = ImageARGB(width: self.yPlane.width, height: self.yPlane.height)
|
||||
self.toARGB(target: resultImage)
|
||||
return resultImage
|
||||
}
|
||||
}
|
||||
|
||||
final class DctData {
|
||||
let quality: Int
|
||||
let dctData: Data
|
||||
let idctData: Data
|
||||
|
||||
init(quality: Int) {
|
||||
self.quality = quality
|
||||
self.dctData = generateForwardDctData(Int32(quality))!
|
||||
self.idctData = generateInverseDctData(Int32(quality))!
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageYUVA420 {
|
||||
func dct(dctData: DctData, target: DctCoefficientsYUVA420) {
|
||||
precondition(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height)
|
||||
|
||||
for i in 0 ..< 4 {
|
||||
let sourcePlane: ImagePlane
|
||||
let targetPlane: DctCoefficientPlane
|
||||
switch i {
|
||||
case 0:
|
||||
sourcePlane = self.yPlane
|
||||
targetPlane = target.yPlane
|
||||
case 1:
|
||||
sourcePlane = self.uPlane
|
||||
targetPlane = target.uPlane
|
||||
case 2:
|
||||
sourcePlane = self.vPlane
|
||||
targetPlane = target.vPlane
|
||||
case 3:
|
||||
sourcePlane = self.aPlane
|
||||
targetPlane = target.aPlane
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
sourcePlane.data.withUnsafeBytes { sourceBytes in
|
||||
let sourcePixels = sourceBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
targetPlane.data.withUnsafeMutableBytes { bytes in
|
||||
let coefficients = bytes.baseAddress!.assumingMemoryBound(to: UInt16.self)
|
||||
|
||||
performForwardDct(sourcePixels, coefficients, Int32(sourcePlane.width), Int32(sourcePlane.height), Int32(sourcePlane.bytesPerRow), dctData.dctData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dct(dctData: DctData) -> DctCoefficientsYUVA420 {
|
||||
let results = DctCoefficientsYUVA420(width: self.yPlane.width, height: self.yPlane.height)
|
||||
self.dct(dctData: dctData, target: results)
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
extension DctCoefficientsYUVA420 {
|
||||
func idct(dctData: DctData, target: ImageYUVA420) {
|
||||
precondition(self.yPlane.width == target.yPlane.width && self.yPlane.height == target.yPlane.height)
|
||||
|
||||
for i in 0 ..< 4 {
|
||||
let sourcePlane: DctCoefficientPlane
|
||||
let targetPlane: ImagePlane
|
||||
switch i {
|
||||
case 0:
|
||||
sourcePlane = self.yPlane
|
||||
targetPlane = target.yPlane
|
||||
case 1:
|
||||
sourcePlane = self.uPlane
|
||||
targetPlane = target.uPlane
|
||||
case 2:
|
||||
sourcePlane = self.vPlane
|
||||
targetPlane = target.vPlane
|
||||
case 3:
|
||||
sourcePlane = self.aPlane
|
||||
targetPlane = target.aPlane
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
sourcePlane.data.withUnsafeBytes { sourceBytes in
|
||||
let coefficients = sourceBytes.baseAddress!.assumingMemoryBound(to: UInt16.self)
|
||||
|
||||
targetPlane.data.withUnsafeMutableBytes { bytes in
|
||||
let pixels = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
performInverseDct(coefficients, pixels, Int32(sourcePlane.width), Int32(sourcePlane.height), Int32(targetPlane.bytesPerRow), Int32(sourcePlane.width), dctData.idctData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func idct(dctData: DctData) -> ImageYUVA420 {
|
||||
let resultImage = ImageYUVA420(width: self.yPlane.width, height: self.yPlane.height)
|
||||
self.idct(dctData: dctData, target: resultImage)
|
||||
return resultImage
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user