mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Entity input: improved animation cache and rendering
This commit is contained in:
parent
0f1b382265
commit
c112bc5146
@ -249,6 +249,7 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
||||
case monospace
|
||||
case textMention(EnginePeer.Id)
|
||||
case textUrl(String)
|
||||
case customEmoji(stickerPack: StickerPackReference, fileId: Int64)
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
@ -266,6 +267,10 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
||||
case 4:
|
||||
let url = (try? container.decode(String.self, forKey: "url")) ?? ""
|
||||
self = .textUrl(url)
|
||||
case 5:
|
||||
let stickerPack = try container.decode(StickerPackReference.self, forKey: "s")
|
||||
let fileId = try container.decode(Int64.self, forKey: "f")
|
||||
self = .customEmoji(stickerPack: stickerPack, fileId: fileId)
|
||||
default:
|
||||
assertionFailure()
|
||||
self = .bold
|
||||
@ -287,6 +292,10 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable {
|
||||
case let .textUrl(url):
|
||||
try container.encode(4 as Int32, forKey: "t")
|
||||
try container.encode(url, forKey: "url")
|
||||
case let .customEmoji(stickerPack, fileId):
|
||||
try container.encode(5 as Int32, forKey: "t")
|
||||
try container.encode(stickerPack, forKey: "s")
|
||||
try container.encode(fileId, forKey: "f")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -352,6 +361,8 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textMention(value.peerId), range: range.location ..< (range.location + range.length)))
|
||||
} else if key == ChatTextInputAttributes.textUrl, let value = value as? ChatTextInputTextUrlAttribute {
|
||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length)))
|
||||
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||
parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: value.stickerPack, fileId: value.fileId), range: range.location ..< (range.location + range.length)))
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -388,6 +399,8 @@ public struct ChatTextInputStateText: Codable, Equatable {
|
||||
result.addAttribute(ChatTextInputAttributes.textMention, value: ChatTextInputTextMentionAttribute(peerId: id), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
case let .textUrl(url):
|
||||
result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
case let .customEmoji(stickerPack, fileId):
|
||||
result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count))
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@ -39,16 +39,17 @@
|
||||
|
||||
@implementation ASCustomLayoutManager
|
||||
|
||||
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
|
||||
/*CGGlyph glyph = [self glyphAtIndex:glyphsToShow.location];
|
||||
if (glyph) {
|
||||
- (void)showCGGlyphs:(const CGGlyph *)glyphs positions:(const CGPoint *)positions count:(NSUInteger)glyphCount font:(UIFont *)font matrix:(CGAffineTransform)textMatrix attributes:(NSDictionary<NSAttributedStringKey,id> *)attributes inContext:(CGContextRef)graphicsContext {
|
||||
for (NSUInteger i = 0; i < glyphCount; i++) {
|
||||
if (attributes[@"Attribute__CustomEmoji"] != nil) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CGRect bounds = [self boundingRectForGlyphRange:glyphsToShow inTextContainer:[self textContainerForGlyphAtIndex:glyphsToShow.location effectiveRange:nil]];
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSetFillColorWithColor(context, [UIColor grayColor].CGColor);
|
||||
CGContextFillRect(context, bounds);*/
|
||||
[super showCGGlyphs:&glyphs[i] positions:&positions[i] count:1 font:font matrix:textMatrix attributes:attributes inContext:graphicsContext];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
|
||||
[super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
|
||||
}
|
||||
|
||||
|
||||
@ -242,7 +242,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
private var spoilersRevealed = false
|
||||
|
||||
private var emojiViewProvider: ((String) -> UIView)?
|
||||
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||
|
||||
private var maxCaptionLength: Int32?
|
||||
|
||||
|
||||
@ -144,7 +144,7 @@ public final class ChatPanelInterfaceInteraction {
|
||||
public let displayCopyProtectionTip: (ASDisplayNode, Bool) -> Void
|
||||
public let openWebView: (String, String, Bool, Bool) -> Void
|
||||
public let updateShowWebView: ((Bool) -> Bool) -> Void
|
||||
public let insertText: (String) -> Void
|
||||
public let insertText: (NSAttributedString) -> Void
|
||||
public let backwardsDeleteText: () -> Void
|
||||
public let chatController: () -> ViewController?
|
||||
public let statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
@ -240,7 +240,7 @@ public final class ChatPanelInterfaceInteraction {
|
||||
displayCopyProtectionTip: @escaping (ASDisplayNode, Bool) -> Void,
|
||||
openWebView: @escaping (String, String, Bool, Bool) -> Void,
|
||||
updateShowWebView: @escaping ((Bool) -> Bool) -> Void,
|
||||
insertText: @escaping (String) -> Void,
|
||||
insertText: @escaping (NSAttributedString) -> Void,
|
||||
backwardsDeleteText: @escaping () -> Void,
|
||||
chatController: @escaping () -> ViewController?,
|
||||
statuses: ChatPanelInterfaceInteractionStatuses?
|
||||
|
||||
@ -19,6 +19,7 @@ public final class ManagedFile {
|
||||
private let queue: Queue?
|
||||
private let fd: Int32
|
||||
private let mode: Mode
|
||||
private var isClosed: Bool = false
|
||||
|
||||
public init?(queue: Queue?, path: String, mode: Mode) {
|
||||
if let queue = queue {
|
||||
@ -51,13 +52,24 @@ public final class ManagedFile {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
if !self.isClosed {
|
||||
close(self.fd)
|
||||
}
|
||||
}
|
||||
|
||||
public func _unsafeClose() {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
close(self.fd)
|
||||
self.isClosed = true
|
||||
}
|
||||
|
||||
public func write(_ data: UnsafeRawPointer, count: Int) -> Int {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
assert(!self.isClosed)
|
||||
return wrappedWrite(self.fd, data, count)
|
||||
}
|
||||
|
||||
@ -65,6 +77,7 @@ public final class ManagedFile {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
assert(!self.isClosed)
|
||||
return wrappedRead(self.fd, data, count)
|
||||
}
|
||||
|
||||
@ -72,6 +85,7 @@ public final class ManagedFile {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
assert(!self.isClosed)
|
||||
var result = Data(count: count)
|
||||
result.withUnsafeMutableBytes { buffer -> Void in
|
||||
guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
@ -87,6 +101,7 @@ public final class ManagedFile {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
assert(!self.isClosed)
|
||||
lseek(self.fd, position, SEEK_SET)
|
||||
}
|
||||
|
||||
@ -94,6 +109,7 @@ public final class ManagedFile {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
assert(!self.isClosed)
|
||||
ftruncate(self.fd, count)
|
||||
}
|
||||
|
||||
@ -101,6 +117,7 @@ public final class ManagedFile {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
assert(!self.isClosed)
|
||||
var value = stat()
|
||||
if fstat(self.fd, &value) == 0 {
|
||||
return value.st_size
|
||||
@ -113,6 +130,7 @@ public final class ManagedFile {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
assert(!self.isClosed)
|
||||
|
||||
return lseek(self.fd, 0, SEEK_CUR);
|
||||
}
|
||||
@ -121,6 +139,7 @@ public final class ManagedFile {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
assert(!self.isClosed)
|
||||
fsync(self.fd)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1239,6 +1239,27 @@ public final class MediaBox {
|
||||
}
|
||||
}
|
||||
|
||||
func processRecursive(directoryPath: String, subdirectoryPath: String) {
|
||||
if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: directoryPath), includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) {
|
||||
loop: for url in enumerator {
|
||||
if let url = url as? URL {
|
||||
if let prefix = url.lastPathComponent.components(separatedBy: ":").first, excludePrefixes.contains(prefix) {
|
||||
continue loop
|
||||
}
|
||||
|
||||
if let isDirectory = (try? url.resourceValues(forKeys: Set([.isDirectoryKey])))?.isDirectory, isDirectory {
|
||||
processRecursive(directoryPath: url.path, subdirectoryPath: subdirectoryPath + "/\(url.lastPathComponent)")
|
||||
} else if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 {
|
||||
paths.append("\(subdirectoryPath)/" + url.lastPathComponent)
|
||||
cacheResult += Int64(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processRecursive(directoryPath: self.basePath + "/animation-cache", subdirectoryPath: "animation-cache")
|
||||
|
||||
if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/short-cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) {
|
||||
loop: for url in enumerator {
|
||||
if let url = url as? URL {
|
||||
|
||||
@ -108,7 +108,7 @@ private final class MediaBoxFileMap {
|
||||
return nil
|
||||
}
|
||||
|
||||
if count < 0 || length < 4 + 4 + count * 2 * 4 {
|
||||
if count < 0 || UInt64(length) < 4 + 4 + UInt64(count) * 2 * 4 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -433,6 +433,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[1827637959] = { return Api.MessageEntity.parse_messageEntityBotCommand($0) }
|
||||
dict[1280209983] = { return Api.MessageEntity.parse_messageEntityCashtag($0) }
|
||||
dict[681706865] = { return Api.MessageEntity.parse_messageEntityCode($0) }
|
||||
dict[-727707947] = { return Api.MessageEntity.parse_messageEntityCustomEmoji($0) }
|
||||
dict[1692693954] = { return Api.MessageEntity.parse_messageEntityEmail($0) }
|
||||
dict[1868782349] = { return Api.MessageEntity.parse_messageEntityHashtag($0) }
|
||||
dict[-2106619040] = { return Api.MessageEntity.parse_messageEntityItalic($0) }
|
||||
|
||||
@ -7,6 +7,7 @@ public extension Api {
|
||||
case messageEntityBotCommand(offset: Int32, length: Int32)
|
||||
case messageEntityCashtag(offset: Int32, length: Int32)
|
||||
case messageEntityCode(offset: Int32, length: Int32)
|
||||
case messageEntityCustomEmoji(offset: Int32, length: Int32, stickerset: Api.InputStickerSet, documentId: Int64)
|
||||
case messageEntityEmail(offset: Int32, length: Int32)
|
||||
case messageEntityHashtag(offset: Int32, length: Int32)
|
||||
case messageEntityItalic(offset: Int32, length: Int32)
|
||||
@ -73,6 +74,15 @@ public extension Api {
|
||||
serializeInt32(offset, buffer: buffer, boxed: false)
|
||||
serializeInt32(length, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .messageEntityCustomEmoji(let offset, let length, let stickerset, let documentId):
|
||||
if boxed {
|
||||
buffer.appendInt32(-727707947)
|
||||
}
|
||||
serializeInt32(offset, buffer: buffer, boxed: false)
|
||||
serializeInt32(length, buffer: buffer, boxed: false)
|
||||
stickerset.serialize(buffer, true)
|
||||
serializeInt64(documentId, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .messageEntityEmail(let offset, let length):
|
||||
if boxed {
|
||||
buffer.appendInt32(1692693954)
|
||||
@ -186,6 +196,8 @@ public extension Api {
|
||||
return ("messageEntityCashtag", [("offset", String(describing: offset)), ("length", String(describing: length))])
|
||||
case .messageEntityCode(let offset, let length):
|
||||
return ("messageEntityCode", [("offset", String(describing: offset)), ("length", String(describing: length))])
|
||||
case .messageEntityCustomEmoji(let offset, let length, let stickerset, let documentId):
|
||||
return ("messageEntityCustomEmoji", [("offset", String(describing: offset)), ("length", String(describing: length)), ("stickerset", String(describing: stickerset)), ("documentId", String(describing: documentId))])
|
||||
case .messageEntityEmail(let offset, let length):
|
||||
return ("messageEntityEmail", [("offset", String(describing: offset)), ("length", String(describing: length))])
|
||||
case .messageEntityHashtag(let offset, let length):
|
||||
@ -318,6 +330,28 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_messageEntityCustomEmoji(_ reader: BufferReader) -> MessageEntity? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: Api.InputStickerSet?
|
||||
if let signature = reader.readInt32() {
|
||||
_3 = Api.parse(reader, signature: signature) as? Api.InputStickerSet
|
||||
}
|
||||
var _4: Int64?
|
||||
_4 = reader.readInt64()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 {
|
||||
return Api.MessageEntity.messageEntityCustomEmoji(offset: _1!, length: _2!, stickerset: _3!, documentId: _4!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
||||
@ -374,8 +374,10 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes
|
||||
result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BankCard))
|
||||
case let .messageEntitySpoiler(offset, length):
|
||||
result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Spoiler))
|
||||
/*case let .messageEntityAnimatedEmoji(offset, length):
|
||||
result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .AnimatedEmoji(nil)))*/
|
||||
case let .messageEntityCustomEmoji(offset, length, stickerset, documentId):
|
||||
if let stickerPack = StickerPackReference(apiInputSet: stickerset) {
|
||||
result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .CustomEmoji(stickerPack: stickerPack, fileId: documentId)))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@ -48,9 +48,8 @@ func apiEntitiesFromMessageTextEntities(_ entities: [MessageTextEntity], associa
|
||||
apiEntities.append(.messageEntityBankCard(offset: offset, length: length))
|
||||
case .Spoiler:
|
||||
apiEntities.append(.messageEntitySpoiler(offset: offset, length: length))
|
||||
case .AnimatedEmoji:
|
||||
//apiEntities.append(.messageEntityAnimatedEmoji(offset: offset, length: length))
|
||||
break
|
||||
case let .CustomEmoji(stickerPack, fileId):
|
||||
apiEntities.append(.messageEntityCustomEmoji(offset: offset, length: length, stickerset: stickerPack.apiInputStickerSet, documentId: fileId))
|
||||
case .Custom:
|
||||
break
|
||||
}
|
||||
|
||||
@ -715,7 +715,7 @@ private func decryptedEntities73(_ entities: [MessageTextEntity]?) -> [SecretApi
|
||||
break
|
||||
case .Spoiler:
|
||||
break
|
||||
case .AnimatedEmoji:
|
||||
case .CustomEmoji:
|
||||
break
|
||||
case .Custom:
|
||||
break
|
||||
@ -768,7 +768,7 @@ private func decryptedEntities101(_ entities: [MessageTextEntity]?) -> [SecretAp
|
||||
break
|
||||
case .Spoiler:
|
||||
break
|
||||
case .AnimatedEmoji:
|
||||
case .CustomEmoji:
|
||||
break
|
||||
case .Custom:
|
||||
break
|
||||
|
||||
@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
|
||||
|
||||
public class Serialization: NSObject, MTSerialization {
|
||||
public func currentLayer() -> UInt {
|
||||
return 143
|
||||
return 144
|
||||
}
|
||||
|
||||
public func parseMessage(_ data: Data!) -> Any! {
|
||||
|
||||
@ -12,7 +12,7 @@ private let typeHintFileIsLarge: Int32 = 7
|
||||
private let typeHintIsValidated: Int32 = 8
|
||||
private let typeNoPremium: Int32 = 9
|
||||
|
||||
public enum StickerPackReference: PostboxCoding, Hashable, Equatable {
|
||||
public enum StickerPackReference: PostboxCoding, Hashable, Equatable, Codable {
|
||||
case id(id: Int64, accessHash: Int64)
|
||||
case name(String)
|
||||
case animatedEmoji
|
||||
@ -37,6 +37,27 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public init(decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
let discriminator = try container.decode(Int32.self, forKey: "r")
|
||||
switch discriminator {
|
||||
case 0:
|
||||
self = .id(id: try container.decode(Int64.self, forKey: "i"), accessHash: try container.decode(Int64.self, forKey: "h"))
|
||||
case 1:
|
||||
self = .name(try container.decode(String.self, forKey: "n"))
|
||||
case 2:
|
||||
self = .animatedEmoji
|
||||
case 3:
|
||||
self = .dice((try? container.decode(String.self, forKey: "e")) ?? "🎲")
|
||||
case 4:
|
||||
self = .animatedEmojiAnimations
|
||||
default:
|
||||
self = .name("")
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
switch self {
|
||||
case let .id(id, accessHash):
|
||||
@ -56,6 +77,27 @@ public enum StickerPackReference: PostboxCoding, Hashable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: StringCodingKey.self)
|
||||
|
||||
switch self {
|
||||
case let .id(id, accessHash):
|
||||
try container.encode(0 as Int32, forKey: "r")
|
||||
try container.encode(id, forKey: "i")
|
||||
try container.encode(accessHash, forKey: "h")
|
||||
case let .name(name):
|
||||
try container.encode(1 as Int32, forKey: "r")
|
||||
try container.encode(name, forKey: "n")
|
||||
case .animatedEmoji:
|
||||
try container.encode(2 as Int32, forKey: "r")
|
||||
case let .dice(emoji):
|
||||
try container.encode(3 as Int32, forKey: "r")
|
||||
try container.encode(emoji, forKey: "e")
|
||||
case .animatedEmojiAnimations:
|
||||
try container.encode(4 as Int32, forKey: "r")
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: StickerPackReference, rhs: StickerPackReference) -> Bool {
|
||||
switch lhs {
|
||||
case let .id(id, accessHash):
|
||||
|
||||
@ -21,7 +21,7 @@ public enum MessageTextEntityType: Equatable {
|
||||
case Underline
|
||||
case BankCard
|
||||
case Spoiler
|
||||
case AnimatedEmoji(MediaId?)
|
||||
case CustomEmoji(stickerPack: StickerPackReference, fileId: Int64)
|
||||
case Custom(type: CustomEntityType)
|
||||
}
|
||||
|
||||
@ -73,7 +73,11 @@ public struct MessageTextEntity: PostboxCoding, Codable, Equatable {
|
||||
case 17:
|
||||
self.type = .Spoiler
|
||||
case 18:
|
||||
self.type = .AnimatedEmoji(decoder.decodeObjectForKey("mediaId") as? MediaId)
|
||||
if let stickerPack = decoder.decodeObjectForKey("s", decoder: { StickerPackReference(decoder: $0) }) as? StickerPackReference {
|
||||
self.type = .CustomEmoji(stickerPack: stickerPack, fileId: decoder.decodeInt64ForKey("f", orElse: 0))
|
||||
} else {
|
||||
self.type = .Unknown
|
||||
}
|
||||
case Int32.max:
|
||||
self.type = .Custom(type: decoder.decodeInt32ForKey("type", orElse: 0))
|
||||
default:
|
||||
@ -130,7 +134,7 @@ public struct MessageTextEntity: PostboxCoding, Codable, Equatable {
|
||||
case 17:
|
||||
self.type = .Spoiler
|
||||
case 18:
|
||||
self.type = .AnimatedEmoji(try? container.decode(MediaId.self, forKey: "mediaId"))
|
||||
self.type = .CustomEmoji(stickerPack: try container.decode(StickerPackReference.self, forKey: "s"), fileId: try container.decode(Int64.self, forKey: "f"))
|
||||
case Int32.max:
|
||||
let customType: Int32 = (try? container.decode(Int32.self, forKey: "type")) ?? 0
|
||||
self.type = .Custom(type: customType)
|
||||
@ -181,13 +185,10 @@ public struct MessageTextEntity: PostboxCoding, Codable, Equatable {
|
||||
encoder.encodeInt32(16, forKey: "_rawValue")
|
||||
case .Spoiler:
|
||||
encoder.encodeInt32(17, forKey: "_rawValue")
|
||||
case let .AnimatedEmoji(mediaId):
|
||||
case let .CustomEmoji(stickerPack, fileId):
|
||||
encoder.encodeInt32(18, forKey: "_rawValue")
|
||||
if let mediaId = mediaId {
|
||||
encoder.encodeObject(mediaId, forKey: "mediaId")
|
||||
} else {
|
||||
encoder.encodeNil(forKey: "mediaId")
|
||||
}
|
||||
encoder.encodeObject(stickerPack, forKey: "s")
|
||||
encoder.encodeInt64(fileId, forKey: "f")
|
||||
case let .Custom(type):
|
||||
encoder.encodeInt32(Int32.max, forKey: "_rawValue")
|
||||
encoder.encodeInt32(type, forKey: "type")
|
||||
@ -238,11 +239,10 @@ public struct MessageTextEntity: PostboxCoding, Codable, Equatable {
|
||||
try container.encode(16 as Int32, forKey: "_rawValue")
|
||||
case .Spoiler:
|
||||
try container.encode(17 as Int32, forKey: "_rawValue")
|
||||
case let .AnimatedEmoji(mediaId):
|
||||
case let .CustomEmoji(stickerPack, fileId):
|
||||
try container.encode(18 as Int32, forKey: "_rawValue")
|
||||
if let mediaId = mediaId {
|
||||
try container.encode(mediaId, forKey: "mediaId")
|
||||
}
|
||||
try container.encode(stickerPack, forKey: "s")
|
||||
try container.encode(fileId, forKey: "f")
|
||||
case let .Custom(type):
|
||||
try container.encode(Int32.max as Int32, forKey: "_rawValue")
|
||||
try container.encode(type as Int32, forKey: "type")
|
||||
|
||||
@ -285,6 +285,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
|
||||
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC",
|
||||
|
||||
@ -13,6 +13,7 @@ swift_library(
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/CryptoUtils:CryptoUtils",
|
||||
"//submodules/ManagedFile:ManagedFile",
|
||||
"//submodules/TelegramUI/Components/AnimationCache/DCT:DCT",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
23
submodules/TelegramUI/Components/AnimationCache/DCT/BUILD
Normal file
23
submodules/TelegramUI/Components/AnimationCache/DCT/BUILD
Normal file
@ -0,0 +1,23 @@
|
||||
|
||||
objc_library(
|
||||
name = "DCT",
|
||||
enable_modules = True,
|
||||
module_name = "DCT",
|
||||
srcs = glob([
|
||||
"Sources/**/*.m",
|
||||
"Sources/**/*.h",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"PublicHeaders/**/*.h",
|
||||
]),
|
||||
includes = [
|
||||
"PublicHeaders",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
"Accelerate",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -0,0 +1,14 @@
|
||||
#ifndef DctImageTransform_h
|
||||
#define DctImageTransform_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <DCT/YuvConversion.h>
|
||||
|
||||
NSData *generateForwardDctData(int quality);
|
||||
void performForwardDct(uint8_t const *pixels, int16_t *coefficients, int width, int height, int bytesPerRow, NSData *dctData);
|
||||
|
||||
NSData *generateInverseDctData(int quality);
|
||||
void performInverseDct(int16_t const *coefficients, uint8_t *pixels, int width, int height, int coefficientsPerRow, int bytesPerRow, NSData *idctData);
|
||||
|
||||
#endif /* DctImageTransform_h */
|
||||
@ -0,0 +1,9 @@
|
||||
#ifndef YuvConversion_h
|
||||
#define YuvConversion_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow);
|
||||
void combineYUVAPlanesIntoARBB(uint8_t *argb, uint8_t const *inY, uint8_t const *inU, uint8_t const *inV, uint8_t const *inA, int width, int height, int bytesPerRow);
|
||||
|
||||
#endif /* YuvConversion_h */
|
||||
@ -0,0 +1,991 @@
|
||||
#import <DCT/DCT.h>
|
||||
|
||||
typedef long JLONG;
|
||||
|
||||
typedef unsigned char JSAMPLE;
|
||||
#define GETJSAMPLE(value) ((int)(value))
|
||||
|
||||
#define MAXJSAMPLE 255
|
||||
#define CENTERJSAMPLE 128
|
||||
|
||||
typedef short JCOEF;
|
||||
|
||||
typedef unsigned int JDIMENSION;
|
||||
|
||||
#define JPEG_MAX_DIMENSION 65500L /* a tad under 64K to prevent overflows */
|
||||
|
||||
#define MULTIPLIER short /* prefer 16-bit with SIMD for parellelism */
|
||||
|
||||
typedef MULTIPLIER IFAST_MULT_TYPE; /* 16 bits is OK, use short if faster */
|
||||
#define IFAST_SCALE_BITS 2 /* fractional bits in scale factors */
|
||||
|
||||
/* Various constants determining the sizes of things.
|
||||
* All of these are specified by the JPEG standard, so don't change them
|
||||
* if you want to be compatible.
|
||||
*/
|
||||
|
||||
#define DCTSIZE 8 /* The basic DCT block is 8x8 samples */
|
||||
#define DCTSIZE2 64 /* DCTSIZE squared; # of elements in a block */
|
||||
#define NUM_QUANT_TBLS 4 /* Quantization tables are numbered 0..3 */
|
||||
#define NUM_HUFF_TBLS 4 /* Huffman tables are numbered 0..3 */
|
||||
#define NUM_ARITH_TBLS 16 /* Arith-coding tables are numbered 0..15 */
|
||||
#define MAX_COMPS_IN_SCAN 4 /* JPEG limit on # of components in one scan */
|
||||
#define MAX_SAMP_FACTOR 4 /* JPEG limit on sampling factors */
|
||||
/* Unfortunately, some bozo at Adobe saw no reason to be bound by the standard;
|
||||
* the PostScript DCT filter can emit files with many more than 10 blocks/MCU.
|
||||
* If you happen to run across such a file, you can up D_MAX_BLOCKS_IN_MCU
|
||||
* to handle it. We even let you do this from the jconfig.h file. However,
|
||||
* we strongly discourage changing C_MAX_BLOCKS_IN_MCU; just because Adobe
|
||||
* sometimes emits noncompliant files doesn't mean you should too.
|
||||
*/
|
||||
#define C_MAX_BLOCKS_IN_MCU 10 /* compressor's limit on blocks per MCU */
|
||||
#ifndef D_MAX_BLOCKS_IN_MCU
|
||||
#define D_MAX_BLOCKS_IN_MCU 10 /* decompressor's limit on blocks per MCU */
|
||||
#endif
|
||||
|
||||
|
||||
/* Data structures for images (arrays of samples and of DCT coefficients).
|
||||
*/
|
||||
|
||||
typedef JSAMPLE *JSAMPROW; /* ptr to one image row of pixel samples. */
|
||||
typedef JSAMPROW *JSAMPARRAY; /* ptr to some rows (a 2-D sample array) */
|
||||
typedef JSAMPARRAY *JSAMPIMAGE; /* a 3-D sample array: top index is color */
|
||||
|
||||
typedef JCOEF JBLOCK[DCTSIZE2]; /* one block of coefficients */
|
||||
typedef JBLOCK *JBLOCKROW; /* pointer to one row of coefficient blocks */
|
||||
typedef JBLOCKROW *JBLOCKARRAY; /* a 2-D array of coefficient blocks */
|
||||
typedef JBLOCKARRAY *JBLOCKIMAGE; /* a 3-D array of coefficient blocks */
|
||||
|
||||
typedef JCOEF *JCOEFPTR; /* useful in a couple of places */
|
||||
|
||||
#include <arm_neon.h>
|
||||
|
||||
/* jsimd_idct_ifast_neon() performs dequantization and a fast, not so accurate
|
||||
* inverse DCT (Discrete Cosine Transform) on one block of coefficients. It
|
||||
* uses the same calculations and produces exactly the same output as IJG's
|
||||
* original jpeg_idct_ifast() function, which can be found in jidctfst.c.
|
||||
*
|
||||
* Scaled integer constants are used to avoid floating-point arithmetic:
|
||||
* 0.082392200 = 2688 * 2^-15
|
||||
* 0.414213562 = 13568 * 2^-15
|
||||
* 0.847759065 = 27776 * 2^-15
|
||||
* 0.613125930 = 20096 * 2^-15
|
||||
*
|
||||
* See jidctfst.c for further details of the IDCT algorithm. Where possible,
|
||||
* the variable names and comments here in jsimd_idct_ifast_neon() match up
|
||||
* with those in jpeg_idct_ifast().
|
||||
*/
|
||||
|
||||
#define PASS1_BITS 2
|
||||
|
||||
#define F_0_082 2688
|
||||
#define F_0_414 13568
|
||||
#define F_0_847 27776
|
||||
#define F_0_613 20096
|
||||
|
||||
|
||||
__attribute__((aligned(16))) static const int16_t jsimd_idct_ifast_neon_consts[] = {
|
||||
F_0_082, F_0_414, F_0_847, F_0_613
|
||||
};
|
||||
|
||||
#define F_0_382 12544
|
||||
#define F_0_541 17792
|
||||
#define F_0_707 23168
|
||||
#define F_0_306 9984
|
||||
|
||||
|
||||
__attribute__((aligned(16))) static const int16_t jsimd_fdct_ifast_neon_consts[] = {
|
||||
F_0_382, F_0_541, F_0_707, F_0_306
|
||||
};
|
||||
|
||||
typedef short DCTELEM; /* prefer 16 bit with SIMD for parellelism */
|
||||
typedef unsigned short UDCTELEM;
|
||||
typedef unsigned int UDCTELEM2;
|
||||
|
||||
static void jsimd_fdct_ifast_neon(DCTELEM *data) {
|
||||
/* Load an 8x8 block of samples into Neon registers. De-interleaving loads
|
||||
* are used, followed by vuzp to transpose the block such that we have a
|
||||
* column of samples per vector - allowing all rows to be processed at once.
|
||||
*/
|
||||
int16x8x4_t data1 = vld4q_s16(data);
|
||||
int16x8x4_t data2 = vld4q_s16(data + 4 * DCTSIZE);
|
||||
|
||||
int16x8x2_t cols_04 = vuzpq_s16(data1.val[0], data2.val[0]);
|
||||
int16x8x2_t cols_15 = vuzpq_s16(data1.val[1], data2.val[1]);
|
||||
int16x8x2_t cols_26 = vuzpq_s16(data1.val[2], data2.val[2]);
|
||||
int16x8x2_t cols_37 = vuzpq_s16(data1.val[3], data2.val[3]);
|
||||
|
||||
int16x8_t col0 = cols_04.val[0];
|
||||
int16x8_t col1 = cols_15.val[0];
|
||||
int16x8_t col2 = cols_26.val[0];
|
||||
int16x8_t col3 = cols_37.val[0];
|
||||
int16x8_t col4 = cols_04.val[1];
|
||||
int16x8_t col5 = cols_15.val[1];
|
||||
int16x8_t col6 = cols_26.val[1];
|
||||
int16x8_t col7 = cols_37.val[1];
|
||||
|
||||
/* Pass 1: process rows. */
|
||||
|
||||
/* Load DCT conversion constants. */
|
||||
const int16x4_t consts = vld1_s16(jsimd_fdct_ifast_neon_consts);
|
||||
|
||||
int16x8_t tmp0 = vaddq_s16(col0, col7);
|
||||
int16x8_t tmp7 = vsubq_s16(col0, col7);
|
||||
int16x8_t tmp1 = vaddq_s16(col1, col6);
|
||||
int16x8_t tmp6 = vsubq_s16(col1, col6);
|
||||
int16x8_t tmp2 = vaddq_s16(col2, col5);
|
||||
int16x8_t tmp5 = vsubq_s16(col2, col5);
|
||||
int16x8_t tmp3 = vaddq_s16(col3, col4);
|
||||
int16x8_t tmp4 = vsubq_s16(col3, col4);
|
||||
|
||||
/* Even part */
|
||||
int16x8_t tmp10 = vaddq_s16(tmp0, tmp3); /* phase 2 */
|
||||
int16x8_t tmp13 = vsubq_s16(tmp0, tmp3);
|
||||
int16x8_t tmp11 = vaddq_s16(tmp1, tmp2);
|
||||
int16x8_t tmp12 = vsubq_s16(tmp1, tmp2);
|
||||
|
||||
col0 = vaddq_s16(tmp10, tmp11); /* phase 3 */
|
||||
col4 = vsubq_s16(tmp10, tmp11);
|
||||
|
||||
int16x8_t z1 = vqdmulhq_lane_s16(vaddq_s16(tmp12, tmp13), consts, 2);
|
||||
col2 = vaddq_s16(tmp13, z1); /* phase 5 */
|
||||
col6 = vsubq_s16(tmp13, z1);
|
||||
|
||||
/* Odd part */
|
||||
tmp10 = vaddq_s16(tmp4, tmp5); /* phase 2 */
|
||||
tmp11 = vaddq_s16(tmp5, tmp6);
|
||||
tmp12 = vaddq_s16(tmp6, tmp7);
|
||||
|
||||
int16x8_t z5 = vqdmulhq_lane_s16(vsubq_s16(tmp10, tmp12), consts, 0);
|
||||
int16x8_t z2 = vqdmulhq_lane_s16(tmp10, consts, 1);
|
||||
z2 = vaddq_s16(z2, z5);
|
||||
int16x8_t z4 = vqdmulhq_lane_s16(tmp12, consts, 3);
|
||||
z5 = vaddq_s16(tmp12, z5);
|
||||
z4 = vaddq_s16(z4, z5);
|
||||
int16x8_t z3 = vqdmulhq_lane_s16(tmp11, consts, 2);
|
||||
|
||||
int16x8_t z11 = vaddq_s16(tmp7, z3); /* phase 5 */
|
||||
int16x8_t z13 = vsubq_s16(tmp7, z3);
|
||||
|
||||
col5 = vaddq_s16(z13, z2); /* phase 6 */
|
||||
col3 = vsubq_s16(z13, z2);
|
||||
col1 = vaddq_s16(z11, z4);
|
||||
col7 = vsubq_s16(z11, z4);
|
||||
|
||||
/* Transpose to work on columns in pass 2. */
|
||||
int16x8x2_t cols_01 = vtrnq_s16(col0, col1);
|
||||
int16x8x2_t cols_23 = vtrnq_s16(col2, col3);
|
||||
int16x8x2_t cols_45 = vtrnq_s16(col4, col5);
|
||||
int16x8x2_t cols_67 = vtrnq_s16(col6, col7);
|
||||
|
||||
int32x4x2_t cols_0145_l = vtrnq_s32(vreinterpretq_s32_s16(cols_01.val[0]),
|
||||
vreinterpretq_s32_s16(cols_45.val[0]));
|
||||
int32x4x2_t cols_0145_h = vtrnq_s32(vreinterpretq_s32_s16(cols_01.val[1]),
|
||||
vreinterpretq_s32_s16(cols_45.val[1]));
|
||||
int32x4x2_t cols_2367_l = vtrnq_s32(vreinterpretq_s32_s16(cols_23.val[0]),
|
||||
vreinterpretq_s32_s16(cols_67.val[0]));
|
||||
int32x4x2_t cols_2367_h = vtrnq_s32(vreinterpretq_s32_s16(cols_23.val[1]),
|
||||
vreinterpretq_s32_s16(cols_67.val[1]));
|
||||
|
||||
int32x4x2_t rows_04 = vzipq_s32(cols_0145_l.val[0], cols_2367_l.val[0]);
|
||||
int32x4x2_t rows_15 = vzipq_s32(cols_0145_h.val[0], cols_2367_h.val[0]);
|
||||
int32x4x2_t rows_26 = vzipq_s32(cols_0145_l.val[1], cols_2367_l.val[1]);
|
||||
int32x4x2_t rows_37 = vzipq_s32(cols_0145_h.val[1], cols_2367_h.val[1]);
|
||||
|
||||
int16x8_t row0 = vreinterpretq_s16_s32(rows_04.val[0]);
|
||||
int16x8_t row1 = vreinterpretq_s16_s32(rows_15.val[0]);
|
||||
int16x8_t row2 = vreinterpretq_s16_s32(rows_26.val[0]);
|
||||
int16x8_t row3 = vreinterpretq_s16_s32(rows_37.val[0]);
|
||||
int16x8_t row4 = vreinterpretq_s16_s32(rows_04.val[1]);
|
||||
int16x8_t row5 = vreinterpretq_s16_s32(rows_15.val[1]);
|
||||
int16x8_t row6 = vreinterpretq_s16_s32(rows_26.val[1]);
|
||||
int16x8_t row7 = vreinterpretq_s16_s32(rows_37.val[1]);
|
||||
|
||||
/* Pass 2: process columns. */
|
||||
|
||||
tmp0 = vaddq_s16(row0, row7);
|
||||
tmp7 = vsubq_s16(row0, row7);
|
||||
tmp1 = vaddq_s16(row1, row6);
|
||||
tmp6 = vsubq_s16(row1, row6);
|
||||
tmp2 = vaddq_s16(row2, row5);
|
||||
tmp5 = vsubq_s16(row2, row5);
|
||||
tmp3 = vaddq_s16(row3, row4);
|
||||
tmp4 = vsubq_s16(row3, row4);
|
||||
|
||||
/* Even part */
|
||||
tmp10 = vaddq_s16(tmp0, tmp3); /* phase 2 */
|
||||
tmp13 = vsubq_s16(tmp0, tmp3);
|
||||
tmp11 = vaddq_s16(tmp1, tmp2);
|
||||
tmp12 = vsubq_s16(tmp1, tmp2);
|
||||
|
||||
row0 = vaddq_s16(tmp10, tmp11); /* phase 3 */
|
||||
row4 = vsubq_s16(tmp10, tmp11);
|
||||
|
||||
z1 = vqdmulhq_lane_s16(vaddq_s16(tmp12, tmp13), consts, 2);
|
||||
row2 = vaddq_s16(tmp13, z1); /* phase 5 */
|
||||
row6 = vsubq_s16(tmp13, z1);
|
||||
|
||||
/* Odd part */
|
||||
tmp10 = vaddq_s16(tmp4, tmp5); /* phase 2 */
|
||||
tmp11 = vaddq_s16(tmp5, tmp6);
|
||||
tmp12 = vaddq_s16(tmp6, tmp7);
|
||||
|
||||
z5 = vqdmulhq_lane_s16(vsubq_s16(tmp10, tmp12), consts, 0);
|
||||
z2 = vqdmulhq_lane_s16(tmp10, consts, 1);
|
||||
z2 = vaddq_s16(z2, z5);
|
||||
z4 = vqdmulhq_lane_s16(tmp12, consts, 3);
|
||||
z5 = vaddq_s16(tmp12, z5);
|
||||
z4 = vaddq_s16(z4, z5);
|
||||
z3 = vqdmulhq_lane_s16(tmp11, consts, 2);
|
||||
|
||||
z11 = vaddq_s16(tmp7, z3); /* phase 5 */
|
||||
z13 = vsubq_s16(tmp7, z3);
|
||||
|
||||
row5 = vaddq_s16(z13, z2); /* phase 6 */
|
||||
row3 = vsubq_s16(z13, z2);
|
||||
row1 = vaddq_s16(z11, z4);
|
||||
row7 = vsubq_s16(z11, z4);
|
||||
|
||||
vst1q_s16(data + 0 * DCTSIZE, row0);
|
||||
vst1q_s16(data + 1 * DCTSIZE, row1);
|
||||
vst1q_s16(data + 2 * DCTSIZE, row2);
|
||||
vst1q_s16(data + 3 * DCTSIZE, row3);
|
||||
vst1q_s16(data + 4 * DCTSIZE, row4);
|
||||
vst1q_s16(data + 5 * DCTSIZE, row5);
|
||||
vst1q_s16(data + 6 * DCTSIZE, row6);
|
||||
vst1q_s16(data + 7 * DCTSIZE, row7);
|
||||
}
|
||||
|
||||
static void jsimd_idct_ifast_neon(void *dct_table, JCOEFPTR coef_block,
|
||||
JSAMPROW output_buf)
|
||||
{
|
||||
IFAST_MULT_TYPE *quantptr = dct_table;
|
||||
|
||||
/* Load DCT coefficients. */
|
||||
int16x8_t row0 = vld1q_s16(coef_block + 0 * DCTSIZE);
|
||||
int16x8_t row1 = vld1q_s16(coef_block + 1 * DCTSIZE);
|
||||
int16x8_t row2 = vld1q_s16(coef_block + 2 * DCTSIZE);
|
||||
int16x8_t row3 = vld1q_s16(coef_block + 3 * DCTSIZE);
|
||||
int16x8_t row4 = vld1q_s16(coef_block + 4 * DCTSIZE);
|
||||
int16x8_t row5 = vld1q_s16(coef_block + 5 * DCTSIZE);
|
||||
int16x8_t row6 = vld1q_s16(coef_block + 6 * DCTSIZE);
|
||||
int16x8_t row7 = vld1q_s16(coef_block + 7 * DCTSIZE);
|
||||
|
||||
/* Load quantization table values for DC coefficients. */
|
||||
int16x8_t quant_row0 = vld1q_s16(quantptr + 0 * DCTSIZE);
|
||||
/* Dequantize DC coefficients. */
|
||||
row0 = vmulq_s16(row0, quant_row0);
|
||||
|
||||
/* Construct bitmap to test if all AC coefficients are 0. */
|
||||
int16x8_t bitmap = vorrq_s16(row1, row2);
|
||||
bitmap = vorrq_s16(bitmap, row3);
|
||||
bitmap = vorrq_s16(bitmap, row4);
|
||||
bitmap = vorrq_s16(bitmap, row5);
|
||||
bitmap = vorrq_s16(bitmap, row6);
|
||||
bitmap = vorrq_s16(bitmap, row7);
|
||||
|
||||
int64_t left_ac_bitmap = vgetq_lane_s64(vreinterpretq_s64_s16(bitmap), 0);
|
||||
int64_t right_ac_bitmap = vgetq_lane_s64(vreinterpretq_s64_s16(bitmap), 1);
|
||||
|
||||
/* Load IDCT conversion constants. */
|
||||
const int16x4_t consts = vld1_s16(jsimd_idct_ifast_neon_consts);
|
||||
|
||||
if (left_ac_bitmap == 0 && right_ac_bitmap == 0) {
|
||||
/* All AC coefficients are zero.
|
||||
* Compute DC values and duplicate into vectors.
|
||||
*/
|
||||
int16x8_t dcval = row0;
|
||||
row1 = dcval;
|
||||
row2 = dcval;
|
||||
row3 = dcval;
|
||||
row4 = dcval;
|
||||
row5 = dcval;
|
||||
row6 = dcval;
|
||||
row7 = dcval;
|
||||
} else if (left_ac_bitmap == 0) {
|
||||
/* AC coefficients are zero for columns 0, 1, 2, and 3.
|
||||
* Use DC values for these columns.
|
||||
*/
|
||||
int16x4_t dcval = vget_low_s16(row0);
|
||||
|
||||
/* Commence regular fast IDCT computation for columns 4, 5, 6, and 7. */
|
||||
|
||||
/* Load quantization table. */
|
||||
int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE + 4);
|
||||
int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE + 4);
|
||||
int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE + 4);
|
||||
int16x4_t quant_row4 = vld1_s16(quantptr + 4 * DCTSIZE + 4);
|
||||
int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE + 4);
|
||||
int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE + 4);
|
||||
int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE + 4);
|
||||
|
||||
/* Even part: dequantize DCT coefficients. */
|
||||
int16x4_t tmp0 = vget_high_s16(row0);
|
||||
int16x4_t tmp1 = vmul_s16(vget_high_s16(row2), quant_row2);
|
||||
int16x4_t tmp2 = vmul_s16(vget_high_s16(row4), quant_row4);
|
||||
int16x4_t tmp3 = vmul_s16(vget_high_s16(row6), quant_row6);
|
||||
|
||||
int16x4_t tmp10 = vadd_s16(tmp0, tmp2); /* phase 3 */
|
||||
int16x4_t tmp11 = vsub_s16(tmp0, tmp2);
|
||||
|
||||
int16x4_t tmp13 = vadd_s16(tmp1, tmp3); /* phases 5-3 */
|
||||
int16x4_t tmp1_sub_tmp3 = vsub_s16(tmp1, tmp3);
|
||||
int16x4_t tmp12 = vqdmulh_lane_s16(tmp1_sub_tmp3, consts, 1);
|
||||
tmp12 = vadd_s16(tmp12, tmp1_sub_tmp3);
|
||||
tmp12 = vsub_s16(tmp12, tmp13);
|
||||
|
||||
tmp0 = vadd_s16(tmp10, tmp13); /* phase 2 */
|
||||
tmp3 = vsub_s16(tmp10, tmp13);
|
||||
tmp1 = vadd_s16(tmp11, tmp12);
|
||||
tmp2 = vsub_s16(tmp11, tmp12);
|
||||
|
||||
/* Odd part: dequantize DCT coefficients. */
|
||||
int16x4_t tmp4 = vmul_s16(vget_high_s16(row1), quant_row1);
|
||||
int16x4_t tmp5 = vmul_s16(vget_high_s16(row3), quant_row3);
|
||||
int16x4_t tmp6 = vmul_s16(vget_high_s16(row5), quant_row5);
|
||||
int16x4_t tmp7 = vmul_s16(vget_high_s16(row7), quant_row7);
|
||||
|
||||
int16x4_t z13 = vadd_s16(tmp6, tmp5); /* phase 6 */
|
||||
int16x4_t neg_z10 = vsub_s16(tmp5, tmp6);
|
||||
int16x4_t z11 = vadd_s16(tmp4, tmp7);
|
||||
int16x4_t z12 = vsub_s16(tmp4, tmp7);
|
||||
|
||||
tmp7 = vadd_s16(z11, z13); /* phase 5 */
|
||||
int16x4_t z11_sub_z13 = vsub_s16(z11, z13);
|
||||
tmp11 = vqdmulh_lane_s16(z11_sub_z13, consts, 1);
|
||||
tmp11 = vadd_s16(tmp11, z11_sub_z13);
|
||||
|
||||
int16x4_t z10_add_z12 = vsub_s16(z12, neg_z10);
|
||||
int16x4_t z5 = vqdmulh_lane_s16(z10_add_z12, consts, 2);
|
||||
z5 = vadd_s16(z5, z10_add_z12);
|
||||
tmp10 = vqdmulh_lane_s16(z12, consts, 0);
|
||||
tmp10 = vadd_s16(tmp10, z12);
|
||||
tmp10 = vsub_s16(tmp10, z5);
|
||||
tmp12 = vqdmulh_lane_s16(neg_z10, consts, 3);
|
||||
tmp12 = vadd_s16(tmp12, vadd_s16(neg_z10, neg_z10));
|
||||
tmp12 = vadd_s16(tmp12, z5);
|
||||
|
||||
tmp6 = vsub_s16(tmp12, tmp7); /* phase 2 */
|
||||
tmp5 = vsub_s16(tmp11, tmp6);
|
||||
tmp4 = vadd_s16(tmp10, tmp5);
|
||||
|
||||
row0 = vcombine_s16(dcval, vadd_s16(tmp0, tmp7));
|
||||
row7 = vcombine_s16(dcval, vsub_s16(tmp0, tmp7));
|
||||
row1 = vcombine_s16(dcval, vadd_s16(tmp1, tmp6));
|
||||
row6 = vcombine_s16(dcval, vsub_s16(tmp1, tmp6));
|
||||
row2 = vcombine_s16(dcval, vadd_s16(tmp2, tmp5));
|
||||
row5 = vcombine_s16(dcval, vsub_s16(tmp2, tmp5));
|
||||
row4 = vcombine_s16(dcval, vadd_s16(tmp3, tmp4));
|
||||
row3 = vcombine_s16(dcval, vsub_s16(tmp3, tmp4));
|
||||
} else if (right_ac_bitmap == 0) {
|
||||
/* AC coefficients are zero for columns 4, 5, 6, and 7.
|
||||
* Use DC values for these columns.
|
||||
*/
|
||||
int16x4_t dcval = vget_high_s16(row0);
|
||||
|
||||
/* Commence regular fast IDCT computation for columns 0, 1, 2, and 3. */
|
||||
|
||||
/* Load quantization table. */
|
||||
int16x4_t quant_row1 = vld1_s16(quantptr + 1 * DCTSIZE);
|
||||
int16x4_t quant_row2 = vld1_s16(quantptr + 2 * DCTSIZE);
|
||||
int16x4_t quant_row3 = vld1_s16(quantptr + 3 * DCTSIZE);
|
||||
int16x4_t quant_row4 = vld1_s16(quantptr + 4 * DCTSIZE);
|
||||
int16x4_t quant_row5 = vld1_s16(quantptr + 5 * DCTSIZE);
|
||||
int16x4_t quant_row6 = vld1_s16(quantptr + 6 * DCTSIZE);
|
||||
int16x4_t quant_row7 = vld1_s16(quantptr + 7 * DCTSIZE);
|
||||
|
||||
/* Even part: dequantize DCT coefficients. */
|
||||
int16x4_t tmp0 = vget_low_s16(row0);
|
||||
int16x4_t tmp1 = vmul_s16(vget_low_s16(row2), quant_row2);
|
||||
int16x4_t tmp2 = vmul_s16(vget_low_s16(row4), quant_row4);
|
||||
int16x4_t tmp3 = vmul_s16(vget_low_s16(row6), quant_row6);
|
||||
|
||||
int16x4_t tmp10 = vadd_s16(tmp0, tmp2); /* phase 3 */
|
||||
int16x4_t tmp11 = vsub_s16(tmp0, tmp2);
|
||||
|
||||
int16x4_t tmp13 = vadd_s16(tmp1, tmp3); /* phases 5-3 */
|
||||
int16x4_t tmp1_sub_tmp3 = vsub_s16(tmp1, tmp3);
|
||||
int16x4_t tmp12 = vqdmulh_lane_s16(tmp1_sub_tmp3, consts, 1);
|
||||
tmp12 = vadd_s16(tmp12, tmp1_sub_tmp3);
|
||||
tmp12 = vsub_s16(tmp12, tmp13);
|
||||
|
||||
tmp0 = vadd_s16(tmp10, tmp13); /* phase 2 */
|
||||
tmp3 = vsub_s16(tmp10, tmp13);
|
||||
tmp1 = vadd_s16(tmp11, tmp12);
|
||||
tmp2 = vsub_s16(tmp11, tmp12);
|
||||
|
||||
/* Odd part: dequantize DCT coefficients. */
|
||||
int16x4_t tmp4 = vmul_s16(vget_low_s16(row1), quant_row1);
|
||||
int16x4_t tmp5 = vmul_s16(vget_low_s16(row3), quant_row3);
|
||||
int16x4_t tmp6 = vmul_s16(vget_low_s16(row5), quant_row5);
|
||||
int16x4_t tmp7 = vmul_s16(vget_low_s16(row7), quant_row7);
|
||||
|
||||
int16x4_t z13 = vadd_s16(tmp6, tmp5); /* phase 6 */
|
||||
int16x4_t neg_z10 = vsub_s16(tmp5, tmp6);
|
||||
int16x4_t z11 = vadd_s16(tmp4, tmp7);
|
||||
int16x4_t z12 = vsub_s16(tmp4, tmp7);
|
||||
|
||||
tmp7 = vadd_s16(z11, z13); /* phase 5 */
|
||||
int16x4_t z11_sub_z13 = vsub_s16(z11, z13);
|
||||
tmp11 = vqdmulh_lane_s16(z11_sub_z13, consts, 1);
|
||||
tmp11 = vadd_s16(tmp11, z11_sub_z13);
|
||||
|
||||
int16x4_t z10_add_z12 = vsub_s16(z12, neg_z10);
|
||||
int16x4_t z5 = vqdmulh_lane_s16(z10_add_z12, consts, 2);
|
||||
z5 = vadd_s16(z5, z10_add_z12);
|
||||
tmp10 = vqdmulh_lane_s16(z12, consts, 0);
|
||||
tmp10 = vadd_s16(tmp10, z12);
|
||||
tmp10 = vsub_s16(tmp10, z5);
|
||||
tmp12 = vqdmulh_lane_s16(neg_z10, consts, 3);
|
||||
tmp12 = vadd_s16(tmp12, vadd_s16(neg_z10, neg_z10));
|
||||
tmp12 = vadd_s16(tmp12, z5);
|
||||
|
||||
tmp6 = vsub_s16(tmp12, tmp7); /* phase 2 */
|
||||
tmp5 = vsub_s16(tmp11, tmp6);
|
||||
tmp4 = vadd_s16(tmp10, tmp5);
|
||||
|
||||
row0 = vcombine_s16(vadd_s16(tmp0, tmp7), dcval);
|
||||
row7 = vcombine_s16(vsub_s16(tmp0, tmp7), dcval);
|
||||
row1 = vcombine_s16(vadd_s16(tmp1, tmp6), dcval);
|
||||
row6 = vcombine_s16(vsub_s16(tmp1, tmp6), dcval);
|
||||
row2 = vcombine_s16(vadd_s16(tmp2, tmp5), dcval);
|
||||
row5 = vcombine_s16(vsub_s16(tmp2, tmp5), dcval);
|
||||
row4 = vcombine_s16(vadd_s16(tmp3, tmp4), dcval);
|
||||
row3 = vcombine_s16(vsub_s16(tmp3, tmp4), dcval);
|
||||
} else {
|
||||
/* Some AC coefficients are non-zero; full IDCT calculation required. */
|
||||
|
||||
/* Load quantization table. */
|
||||
int16x8_t quant_row1 = vld1q_s16(quantptr + 1 * DCTSIZE);
|
||||
int16x8_t quant_row2 = vld1q_s16(quantptr + 2 * DCTSIZE);
|
||||
int16x8_t quant_row3 = vld1q_s16(quantptr + 3 * DCTSIZE);
|
||||
int16x8_t quant_row4 = vld1q_s16(quantptr + 4 * DCTSIZE);
|
||||
int16x8_t quant_row5 = vld1q_s16(quantptr + 5 * DCTSIZE);
|
||||
int16x8_t quant_row6 = vld1q_s16(quantptr + 6 * DCTSIZE);
|
||||
int16x8_t quant_row7 = vld1q_s16(quantptr + 7 * DCTSIZE);
|
||||
|
||||
/* Even part: dequantize DCT coefficients. */
|
||||
int16x8_t tmp0 = row0;
|
||||
int16x8_t tmp1 = vmulq_s16(row2, quant_row2);
|
||||
int16x8_t tmp2 = vmulq_s16(row4, quant_row4);
|
||||
int16x8_t tmp3 = vmulq_s16(row6, quant_row6);
|
||||
|
||||
int16x8_t tmp10 = vaddq_s16(tmp0, tmp2); /* phase 3 */
|
||||
int16x8_t tmp11 = vsubq_s16(tmp0, tmp2);
|
||||
|
||||
int16x8_t tmp13 = vaddq_s16(tmp1, tmp3); /* phases 5-3 */
|
||||
int16x8_t tmp1_sub_tmp3 = vsubq_s16(tmp1, tmp3);
|
||||
int16x8_t tmp12 = vqdmulhq_lane_s16(tmp1_sub_tmp3, consts, 1);
|
||||
tmp12 = vaddq_s16(tmp12, tmp1_sub_tmp3);
|
||||
tmp12 = vsubq_s16(tmp12, tmp13);
|
||||
|
||||
tmp0 = vaddq_s16(tmp10, tmp13); /* phase 2 */
|
||||
tmp3 = vsubq_s16(tmp10, tmp13);
|
||||
tmp1 = vaddq_s16(tmp11, tmp12);
|
||||
tmp2 = vsubq_s16(tmp11, tmp12);
|
||||
|
||||
/* Odd part: dequantize DCT coefficients. */
|
||||
int16x8_t tmp4 = vmulq_s16(row1, quant_row1);
|
||||
int16x8_t tmp5 = vmulq_s16(row3, quant_row3);
|
||||
int16x8_t tmp6 = vmulq_s16(row5, quant_row5);
|
||||
int16x8_t tmp7 = vmulq_s16(row7, quant_row7);
|
||||
|
||||
int16x8_t z13 = vaddq_s16(tmp6, tmp5); /* phase 6 */
|
||||
int16x8_t neg_z10 = vsubq_s16(tmp5, tmp6);
|
||||
int16x8_t z11 = vaddq_s16(tmp4, tmp7);
|
||||
int16x8_t z12 = vsubq_s16(tmp4, tmp7);
|
||||
|
||||
tmp7 = vaddq_s16(z11, z13); /* phase 5 */
|
||||
int16x8_t z11_sub_z13 = vsubq_s16(z11, z13);
|
||||
tmp11 = vqdmulhq_lane_s16(z11_sub_z13, consts, 1);
|
||||
tmp11 = vaddq_s16(tmp11, z11_sub_z13);
|
||||
|
||||
int16x8_t z10_add_z12 = vsubq_s16(z12, neg_z10);
|
||||
int16x8_t z5 = vqdmulhq_lane_s16(z10_add_z12, consts, 2);
|
||||
z5 = vaddq_s16(z5, z10_add_z12);
|
||||
tmp10 = vqdmulhq_lane_s16(z12, consts, 0);
|
||||
tmp10 = vaddq_s16(tmp10, z12);
|
||||
tmp10 = vsubq_s16(tmp10, z5);
|
||||
tmp12 = vqdmulhq_lane_s16(neg_z10, consts, 3);
|
||||
tmp12 = vaddq_s16(tmp12, vaddq_s16(neg_z10, neg_z10));
|
||||
tmp12 = vaddq_s16(tmp12, z5);
|
||||
|
||||
tmp6 = vsubq_s16(tmp12, tmp7); /* phase 2 */
|
||||
tmp5 = vsubq_s16(tmp11, tmp6);
|
||||
tmp4 = vaddq_s16(tmp10, tmp5);
|
||||
|
||||
row0 = vaddq_s16(tmp0, tmp7);
|
||||
row7 = vsubq_s16(tmp0, tmp7);
|
||||
row1 = vaddq_s16(tmp1, tmp6);
|
||||
row6 = vsubq_s16(tmp1, tmp6);
|
||||
row2 = vaddq_s16(tmp2, tmp5);
|
||||
row5 = vsubq_s16(tmp2, tmp5);
|
||||
row4 = vaddq_s16(tmp3, tmp4);
|
||||
row3 = vsubq_s16(tmp3, tmp4);
|
||||
}
|
||||
|
||||
/* Transpose rows to work on columns in pass 2. */
|
||||
int16x8x2_t rows_01 = vtrnq_s16(row0, row1);
|
||||
int16x8x2_t rows_23 = vtrnq_s16(row2, row3);
|
||||
int16x8x2_t rows_45 = vtrnq_s16(row4, row5);
|
||||
int16x8x2_t rows_67 = vtrnq_s16(row6, row7);
|
||||
|
||||
int32x4x2_t rows_0145_l = vtrnq_s32(vreinterpretq_s32_s16(rows_01.val[0]),
|
||||
vreinterpretq_s32_s16(rows_45.val[0]));
|
||||
int32x4x2_t rows_0145_h = vtrnq_s32(vreinterpretq_s32_s16(rows_01.val[1]),
|
||||
vreinterpretq_s32_s16(rows_45.val[1]));
|
||||
int32x4x2_t rows_2367_l = vtrnq_s32(vreinterpretq_s32_s16(rows_23.val[0]),
|
||||
vreinterpretq_s32_s16(rows_67.val[0]));
|
||||
int32x4x2_t rows_2367_h = vtrnq_s32(vreinterpretq_s32_s16(rows_23.val[1]),
|
||||
vreinterpretq_s32_s16(rows_67.val[1]));
|
||||
|
||||
int32x4x2_t cols_04 = vzipq_s32(rows_0145_l.val[0], rows_2367_l.val[0]);
|
||||
int32x4x2_t cols_15 = vzipq_s32(rows_0145_h.val[0], rows_2367_h.val[0]);
|
||||
int32x4x2_t cols_26 = vzipq_s32(rows_0145_l.val[1], rows_2367_l.val[1]);
|
||||
int32x4x2_t cols_37 = vzipq_s32(rows_0145_h.val[1], rows_2367_h.val[1]);
|
||||
|
||||
int16x8_t col0 = vreinterpretq_s16_s32(cols_04.val[0]);
|
||||
int16x8_t col1 = vreinterpretq_s16_s32(cols_15.val[0]);
|
||||
int16x8_t col2 = vreinterpretq_s16_s32(cols_26.val[0]);
|
||||
int16x8_t col3 = vreinterpretq_s16_s32(cols_37.val[0]);
|
||||
int16x8_t col4 = vreinterpretq_s16_s32(cols_04.val[1]);
|
||||
int16x8_t col5 = vreinterpretq_s16_s32(cols_15.val[1]);
|
||||
int16x8_t col6 = vreinterpretq_s16_s32(cols_26.val[1]);
|
||||
int16x8_t col7 = vreinterpretq_s16_s32(cols_37.val[1]);
|
||||
|
||||
/* 1-D IDCT, pass 2 */
|
||||
|
||||
/* Even part */
|
||||
int16x8_t tmp10 = vaddq_s16(col0, col4);
|
||||
int16x8_t tmp11 = vsubq_s16(col0, col4);
|
||||
|
||||
int16x8_t tmp13 = vaddq_s16(col2, col6);
|
||||
int16x8_t col2_sub_col6 = vsubq_s16(col2, col6);
|
||||
int16x8_t tmp12 = vqdmulhq_lane_s16(col2_sub_col6, consts, 1);
|
||||
tmp12 = vaddq_s16(tmp12, col2_sub_col6);
|
||||
tmp12 = vsubq_s16(tmp12, tmp13);
|
||||
|
||||
int16x8_t tmp0 = vaddq_s16(tmp10, tmp13);
|
||||
int16x8_t tmp3 = vsubq_s16(tmp10, tmp13);
|
||||
int16x8_t tmp1 = vaddq_s16(tmp11, tmp12);
|
||||
int16x8_t tmp2 = vsubq_s16(tmp11, tmp12);
|
||||
|
||||
/* Odd part */
|
||||
int16x8_t z13 = vaddq_s16(col5, col3);
|
||||
int16x8_t neg_z10 = vsubq_s16(col3, col5);
|
||||
int16x8_t z11 = vaddq_s16(col1, col7);
|
||||
int16x8_t z12 = vsubq_s16(col1, col7);
|
||||
|
||||
int16x8_t tmp7 = vaddq_s16(z11, z13); /* phase 5 */
|
||||
int16x8_t z11_sub_z13 = vsubq_s16(z11, z13);
|
||||
tmp11 = vqdmulhq_lane_s16(z11_sub_z13, consts, 1);
|
||||
tmp11 = vaddq_s16(tmp11, z11_sub_z13);
|
||||
|
||||
int16x8_t z10_add_z12 = vsubq_s16(z12, neg_z10);
|
||||
int16x8_t z5 = vqdmulhq_lane_s16(z10_add_z12, consts, 2);
|
||||
z5 = vaddq_s16(z5, z10_add_z12);
|
||||
tmp10 = vqdmulhq_lane_s16(z12, consts, 0);
|
||||
tmp10 = vaddq_s16(tmp10, z12);
|
||||
tmp10 = vsubq_s16(tmp10, z5);
|
||||
tmp12 = vqdmulhq_lane_s16(neg_z10, consts, 3);
|
||||
tmp12 = vaddq_s16(tmp12, vaddq_s16(neg_z10, neg_z10));
|
||||
tmp12 = vaddq_s16(tmp12, z5);
|
||||
|
||||
int16x8_t tmp6 = vsubq_s16(tmp12, tmp7); /* phase 2 */
|
||||
int16x8_t tmp5 = vsubq_s16(tmp11, tmp6);
|
||||
int16x8_t tmp4 = vaddq_s16(tmp10, tmp5);
|
||||
|
||||
col0 = vaddq_s16(tmp0, tmp7);
|
||||
col7 = vsubq_s16(tmp0, tmp7);
|
||||
col1 = vaddq_s16(tmp1, tmp6);
|
||||
col6 = vsubq_s16(tmp1, tmp6);
|
||||
col2 = vaddq_s16(tmp2, tmp5);
|
||||
col5 = vsubq_s16(tmp2, tmp5);
|
||||
col4 = vaddq_s16(tmp3, tmp4);
|
||||
col3 = vsubq_s16(tmp3, tmp4);
|
||||
|
||||
/* Scale down by a factor of 8, narrowing to 8-bit. */
|
||||
int8x16_t cols_01_s8 = vcombine_s8(vqshrn_n_s16(col0, PASS1_BITS + 3),
|
||||
vqshrn_n_s16(col1, PASS1_BITS + 3));
|
||||
int8x16_t cols_45_s8 = vcombine_s8(vqshrn_n_s16(col4, PASS1_BITS + 3),
|
||||
vqshrn_n_s16(col5, PASS1_BITS + 3));
|
||||
int8x16_t cols_23_s8 = vcombine_s8(vqshrn_n_s16(col2, PASS1_BITS + 3),
|
||||
vqshrn_n_s16(col3, PASS1_BITS + 3));
|
||||
int8x16_t cols_67_s8 = vcombine_s8(vqshrn_n_s16(col6, PASS1_BITS + 3),
|
||||
vqshrn_n_s16(col7, PASS1_BITS + 3));
|
||||
/* Clamp to range [0-255]. */
|
||||
uint8x16_t cols_01 =
|
||||
vreinterpretq_u8_s8
|
||||
(vaddq_s8(cols_01_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE))));
|
||||
uint8x16_t cols_45 =
|
||||
vreinterpretq_u8_s8
|
||||
(vaddq_s8(cols_45_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE))));
|
||||
uint8x16_t cols_23 =
|
||||
vreinterpretq_u8_s8
|
||||
(vaddq_s8(cols_23_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE))));
|
||||
uint8x16_t cols_67 =
|
||||
vreinterpretq_u8_s8
|
||||
(vaddq_s8(cols_67_s8, vreinterpretq_s8_u8(vdupq_n_u8(CENTERJSAMPLE))));
|
||||
|
||||
/* Transpose block to prepare for store. */
|
||||
uint32x4x2_t cols_0415 = vzipq_u32(vreinterpretq_u32_u8(cols_01),
|
||||
vreinterpretq_u32_u8(cols_45));
|
||||
uint32x4x2_t cols_2637 = vzipq_u32(vreinterpretq_u32_u8(cols_23),
|
||||
vreinterpretq_u32_u8(cols_67));
|
||||
|
||||
uint8x16x2_t cols_0145 = vtrnq_u8(vreinterpretq_u8_u32(cols_0415.val[0]),
|
||||
vreinterpretq_u8_u32(cols_0415.val[1]));
|
||||
uint8x16x2_t cols_2367 = vtrnq_u8(vreinterpretq_u8_u32(cols_2637.val[0]),
|
||||
vreinterpretq_u8_u32(cols_2637.val[1]));
|
||||
uint16x8x2_t rows_0426 = vtrnq_u16(vreinterpretq_u16_u8(cols_0145.val[0]),
|
||||
vreinterpretq_u16_u8(cols_2367.val[0]));
|
||||
uint16x8x2_t rows_1537 = vtrnq_u16(vreinterpretq_u16_u8(cols_0145.val[1]),
|
||||
vreinterpretq_u16_u8(cols_2367.val[1]));
|
||||
|
||||
uint8x16_t rows_04 = vreinterpretq_u8_u16(rows_0426.val[0]);
|
||||
uint8x16_t rows_15 = vreinterpretq_u8_u16(rows_1537.val[0]);
|
||||
uint8x16_t rows_26 = vreinterpretq_u8_u16(rows_0426.val[1]);
|
||||
uint8x16_t rows_37 = vreinterpretq_u8_u16(rows_1537.val[1]);
|
||||
|
||||
JSAMPROW outptr0 = output_buf + DCTSIZE * 0;
|
||||
JSAMPROW outptr1 = output_buf + DCTSIZE * 1;
|
||||
JSAMPROW outptr2 = output_buf + DCTSIZE * 2;
|
||||
JSAMPROW outptr3 = output_buf + DCTSIZE * 3;
|
||||
JSAMPROW outptr4 = output_buf + DCTSIZE * 4;
|
||||
JSAMPROW outptr5 = output_buf + DCTSIZE * 5;
|
||||
JSAMPROW outptr6 = output_buf + DCTSIZE * 6;
|
||||
JSAMPROW outptr7 = output_buf + DCTSIZE * 7;
|
||||
|
||||
/* Store DCT block to memory. */
|
||||
vst1q_lane_u64((uint64_t *)outptr0, vreinterpretq_u64_u8(rows_04), 0);
|
||||
vst1q_lane_u64((uint64_t *)outptr1, vreinterpretq_u64_u8(rows_15), 0);
|
||||
vst1q_lane_u64((uint64_t *)outptr2, vreinterpretq_u64_u8(rows_26), 0);
|
||||
vst1q_lane_u64((uint64_t *)outptr3, vreinterpretq_u64_u8(rows_37), 0);
|
||||
vst1q_lane_u64((uint64_t *)outptr4, vreinterpretq_u64_u8(rows_04), 1);
|
||||
vst1q_lane_u64((uint64_t *)outptr5, vreinterpretq_u64_u8(rows_15), 1);
|
||||
vst1q_lane_u64((uint64_t *)outptr6, vreinterpretq_u64_u8(rows_26), 1);
|
||||
vst1q_lane_u64((uint64_t *)outptr7, vreinterpretq_u64_u8(rows_37), 1);
|
||||
}
|
||||
|
||||
static int flss(uint16_t val) {
|
||||
int bit;
|
||||
|
||||
bit = 16;
|
||||
|
||||
if (!val)
|
||||
return 0;
|
||||
|
||||
if (!(val & 0xff00)) {
|
||||
bit -= 8;
|
||||
val <<= 8;
|
||||
}
|
||||
if (!(val & 0xf000)) {
|
||||
bit -= 4;
|
||||
val <<= 4;
|
||||
}
|
||||
if (!(val & 0xc000)) {
|
||||
bit -= 2;
|
||||
val <<= 2;
|
||||
}
|
||||
if (!(val & 0x8000)) {
|
||||
bit -= 1;
|
||||
val <<= 1;
|
||||
}
|
||||
|
||||
return bit;
|
||||
}
|
||||
|
||||
static int compute_reciprocal(uint16_t divisor, DCTELEM *dtbl) {
|
||||
UDCTELEM2 fq, fr;
|
||||
UDCTELEM c;
|
||||
int b, r;
|
||||
|
||||
if (divisor == 1) {
|
||||
/* divisor == 1 means unquantized, so these reciprocal/correction/shift
|
||||
* values will cause the C quantization algorithm to act like the
|
||||
* identity function. Since only the C quantization algorithm is used in
|
||||
* these cases, the scale value is irrelevant.
|
||||
*/
|
||||
dtbl[DCTSIZE2 * 0] = (DCTELEM)1; /* reciprocal */
|
||||
dtbl[DCTSIZE2 * 1] = (DCTELEM)0; /* correction */
|
||||
dtbl[DCTSIZE2 * 2] = (DCTELEM)1; /* scale */
|
||||
dtbl[DCTSIZE2 * 3] = -(DCTELEM)(sizeof(DCTELEM) * 8); /* shift */
|
||||
return 0;
|
||||
}
|
||||
|
||||
b = flss(divisor) - 1;
|
||||
r = sizeof(DCTELEM) * 8 + b;
|
||||
|
||||
fq = ((UDCTELEM2)1 << r) / divisor;
|
||||
fr = ((UDCTELEM2)1 << r) % divisor;
|
||||
|
||||
c = divisor / 2; /* for rounding */
|
||||
|
||||
if (fr == 0) { /* divisor is power of two */
|
||||
/* fq will be one bit too large to fit in DCTELEM, so adjust */
|
||||
fq >>= 1;
|
||||
r--;
|
||||
} else if (fr <= (divisor / 2U)) { /* fractional part is < 0.5 */
|
||||
c++;
|
||||
} else { /* fractional part is > 0.5 */
|
||||
fq++;
|
||||
}
|
||||
|
||||
dtbl[DCTSIZE2 * 0] = (DCTELEM)fq; /* reciprocal */
|
||||
dtbl[DCTSIZE2 * 1] = (DCTELEM)c; /* correction + roundfactor */
|
||||
#ifdef WITH_SIMD
|
||||
dtbl[DCTSIZE2 * 2] = (DCTELEM)(1 << (sizeof(DCTELEM) * 8 * 2 - r)); /* scale */
|
||||
#else
|
||||
dtbl[DCTSIZE2 * 2] = 1;
|
||||
#endif
|
||||
dtbl[DCTSIZE2 * 3] = (DCTELEM)r - sizeof(DCTELEM) * 8; /* shift */
|
||||
|
||||
if (r <= 16) return 0;
|
||||
else return 1;
|
||||
}
|
||||
|
||||
#define DESCALE(x, n) RIGHT_SHIFT(x, n)
|
||||
|
||||
|
||||
/* Multiply a DCTELEM variable by an JLONG constant, and immediately
|
||||
* descale to yield a DCTELEM result.
|
||||
*/
|
||||
|
||||
#define MULTIPLY(var, const) ((DCTELEM)DESCALE((var) * (const), CONST_BITS))
|
||||
#define MULTIPLY16V16(var1, var2) ((var1) * (var2))
|
||||
|
||||
static DCTELEM std_luminance_quant_tbl[DCTSIZE2] = {
|
||||
16, 11, 10, 16, 24, 40, 51, 61,
|
||||
12, 12, 14, 19, 26, 58, 60, 55,
|
||||
14, 13, 16, 24, 40, 57, 69, 56,
|
||||
14, 17, 22, 29, 51, 87, 80, 62,
|
||||
18, 22, 37, 56, 68, 109, 103, 77,
|
||||
24, 35, 55, 64, 81, 104, 113, 92,
|
||||
49, 64, 78, 87, 103, 121, 120, 101,
|
||||
72, 92, 95, 98, 112, 100, 103, 99
|
||||
};
|
||||
|
||||
static int jpeg_quality_scaling(int quality)
|
||||
/* Convert a user-specified quality rating to a percentage scaling factor
|
||||
* for an underlying quantization table, using our recommended scaling curve.
|
||||
* The input 'quality' factor should be 0 (terrible) to 100 (very good).
|
||||
*/
|
||||
{
|
||||
/* Safety limit on quality factor. Convert 0 to 1 to avoid zero divide. */
|
||||
if (quality <= 0) quality = 1;
|
||||
if (quality > 100) quality = 100;
|
||||
|
||||
/* The basic table is used as-is (scaling 100) for a quality of 50.
|
||||
* Qualities 50..100 are converted to scaling percentage 200 - 2*Q;
|
||||
* note that at Q=100 the scaling is 0, which will cause jpeg_add_quant_table
|
||||
* to make all the table entries 1 (hence, minimum quantization loss).
|
||||
* Qualities 1..50 are converted to scaling percentage 5000/Q.
|
||||
*/
|
||||
if (quality < 50)
|
||||
quality = 5000 / quality;
|
||||
else
|
||||
quality = 200 - quality * 2;
|
||||
|
||||
return quality;
|
||||
}
|
||||
|
||||
static void jpeg_add_quant_table(DCTELEM *qtable, DCTELEM *basicTable, int scale_factor, bool forceBaseline)
|
||||
/* Define a quantization table equal to the basic_table times
|
||||
* a scale factor (given as a percentage).
|
||||
* If force_baseline is TRUE, the computed quantization table entries
|
||||
* are limited to 1..255 for JPEG baseline compatibility.
|
||||
*/
|
||||
{
|
||||
int i;
|
||||
long temp;
|
||||
|
||||
for (i = 0; i < DCTSIZE2; i++) {
|
||||
temp = ((long)basicTable[i] * scale_factor + 50L) / 100L;
|
||||
/* limit the values to the valid range */
|
||||
if (temp <= 0L) temp = 1L;
|
||||
if (temp > 32767L) temp = 32767L; /* max quantizer needed for 12 bits */
|
||||
if (forceBaseline && temp > 255L)
|
||||
temp = 255L; /* limit to baseline range if requested */
|
||||
qtable[i] = (uint16_t)temp;
|
||||
}
|
||||
}
|
||||
|
||||
static void jpeg_set_quality(DCTELEM *qtable, int quality)
|
||||
/* Set or change the 'quality' (quantization) setting, using default tables.
|
||||
* This is the standard quality-adjusting entry point for typical user
|
||||
* interfaces; only those who want detailed control over quantization tables
|
||||
* would use the preceding three routines directly.
|
||||
*/
|
||||
{
|
||||
/* Convert user 0-100 rating to percentage scaling */
|
||||
quality = jpeg_quality_scaling(quality);
|
||||
|
||||
/* Set up standard quality tables */
|
||||
jpeg_add_quant_table(qtable, std_luminance_quant_tbl, quality, false);
|
||||
}
|
||||
|
||||
static void getDivisors(DCTELEM *dtbl, DCTELEM *qtable) {
|
||||
#define CONST_BITS 14
|
||||
#define RIGHT_SHIFT(x, shft) ((x) >> (shft))
|
||||
|
||||
static const int16_t aanscales[DCTSIZE2] = {
|
||||
/* precomputed values scaled up by 14 bits */
|
||||
16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520,
|
||||
22725, 31521, 29692, 26722, 22725, 17855, 12299, 6270,
|
||||
21407, 29692, 27969, 25172, 21407, 16819, 11585, 5906,
|
||||
19266, 26722, 25172, 22654, 19266, 15137, 10426, 5315,
|
||||
16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520,
|
||||
12873, 17855, 16819, 15137, 12873, 10114, 6967, 3552,
|
||||
8867, 12299, 11585, 10426, 8867, 6967, 4799, 2446,
|
||||
4520, 6270, 5906, 5315, 4520, 3552, 2446, 1247
|
||||
};
|
||||
|
||||
for (int i = 0; i < DCTSIZE2; i++) {
|
||||
if (!compute_reciprocal(
|
||||
DESCALE(MULTIPLY16V16((JLONG)qtable[i],
|
||||
(JLONG)aanscales[i]),
|
||||
CONST_BITS - 3), &dtbl[i])) {
|
||||
//fdct->quantize = quantize;
|
||||
printf("here\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void quantize(JCOEFPTR coef_block, DCTELEM *divisors, DCTELEM *workspace)
|
||||
{
|
||||
int i;
|
||||
DCTELEM temp;
|
||||
JCOEFPTR output_ptr = coef_block;
|
||||
|
||||
UDCTELEM recip, corr;
|
||||
int shift;
|
||||
UDCTELEM2 product;
|
||||
|
||||
for (i = 0; i < DCTSIZE2; i++) {
|
||||
temp = workspace[i];
|
||||
recip = divisors[i + DCTSIZE2 * 0];
|
||||
corr = divisors[i + DCTSIZE2 * 1];
|
||||
shift = divisors[i + DCTSIZE2 * 3];
|
||||
|
||||
if (temp < 0) {
|
||||
temp = -temp;
|
||||
product = (UDCTELEM2)(temp + corr) * recip;
|
||||
product >>= shift + sizeof(DCTELEM) * 8;
|
||||
temp = (DCTELEM)product;
|
||||
temp = -temp;
|
||||
} else {
|
||||
product = (UDCTELEM2)(temp + corr) * recip;
|
||||
product >>= shift + sizeof(DCTELEM) * 8;
|
||||
temp = (DCTELEM)product;
|
||||
}
|
||||
output_ptr[i] = (JCOEF)temp;
|
||||
}
|
||||
}
|
||||
|
||||
NSData *generateForwardDctData(int quality) {
|
||||
NSMutableData *divisors = [[NSMutableData alloc] initWithLength:DCTSIZE2 * 4 * sizeof(DCTELEM)];
|
||||
|
||||
DCTELEM qtable[DCTSIZE2];
|
||||
jpeg_set_quality(qtable, quality);
|
||||
|
||||
getDivisors((DCTELEM *)divisors.mutableBytes, qtable);
|
||||
|
||||
return divisors;
|
||||
}
|
||||
|
||||
NSData *generateInverseDctData(int quality) {
|
||||
NSMutableData *divisors = [[NSMutableData alloc] initWithLength:DCTSIZE2 * sizeof(IFAST_MULT_TYPE)];
|
||||
IFAST_MULT_TYPE *ifmtbl = (IFAST_MULT_TYPE *)divisors.mutableBytes;
|
||||
|
||||
DCTELEM qtable[DCTSIZE2];
|
||||
jpeg_set_quality(qtable, quality);
|
||||
|
||||
#define CONST_BITS 14
|
||||
static const int16_t aanscales[DCTSIZE2] = {
|
||||
/* precomputed values scaled up by 14 bits */
|
||||
16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520,
|
||||
22725, 31521, 29692, 26722, 22725, 17855, 12299, 6270,
|
||||
21407, 29692, 27969, 25172, 21407, 16819, 11585, 5906,
|
||||
19266, 26722, 25172, 22654, 19266, 15137, 10426, 5315,
|
||||
16384, 22725, 21407, 19266, 16384, 12873, 8867, 4520,
|
||||
12873, 17855, 16819, 15137, 12873, 10114, 6967, 3552,
|
||||
8867, 12299, 11585, 10426, 8867, 6967, 4799, 2446,
|
||||
4520, 6270, 5906, 5315, 4520, 3552, 2446, 1247
|
||||
};
|
||||
|
||||
for (int i = 0; i < DCTSIZE2; i++) {
|
||||
ifmtbl[i] = (IFAST_MULT_TYPE)
|
||||
DESCALE(MULTIPLY16V16((JLONG)qtable[i],
|
||||
(JLONG)aanscales[i]),
|
||||
CONST_BITS - IFAST_SCALE_BITS);
|
||||
}
|
||||
|
||||
return divisors;
|
||||
}
|
||||
|
||||
static const int zigZagInv[DCTSIZE2] = {
|
||||
0,1,8,16,9,2,3,10,
|
||||
17,24,32,25,18,11,4,5,
|
||||
12,19,26,33,40,48,41,34,
|
||||
27,20,13,6,7,14,21,28,
|
||||
35,42,49,56,57,50,43,36,
|
||||
29,22,15,23,30,37,44,51,
|
||||
58,59,52,45,38,31,39,46,
|
||||
53,60,61,54,47,55,62,63
|
||||
};
|
||||
|
||||
static const int zigZag[DCTSIZE2] = {
|
||||
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63
|
||||
};
|
||||
|
||||
void performForwardDct(uint8_t const *pixels, int16_t *coefficients, int width, int height, int bytesPerRow, NSData *dctData) {
|
||||
DCTELEM *divisors = (DCTELEM *)dctData.bytes;
|
||||
|
||||
DCTELEM block[DCTSIZE2];
|
||||
JCOEF coefBlock[DCTSIZE2];
|
||||
|
||||
for (int y = 0; y < height; y += DCTSIZE) {
|
||||
for (int x = 0; x < width; x += DCTSIZE) {
|
||||
for (int blockY = 0; blockY < DCTSIZE; blockY++) {
|
||||
for (int blockX = 0; blockX < DCTSIZE; blockX++) {
|
||||
block[blockY * DCTSIZE + blockX] = ((DCTELEM)pixels[(y + blockY) * bytesPerRow + (x + blockX)]) - CENTERJSAMPLE;
|
||||
}
|
||||
}
|
||||
|
||||
jsimd_fdct_ifast_neon(block);
|
||||
|
||||
quantize(coefBlock, divisors, block);
|
||||
|
||||
for (int blockY = 0; blockY < DCTSIZE; blockY++) {
|
||||
for (int blockX = 0; blockX < DCTSIZE; blockX++) {
|
||||
coefficients[(y + blockY) * bytesPerRow + (x + blockX)] = coefBlock[zigZagInv[blockY * DCTSIZE + blockX]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void performInverseDct(int16_t const *coefficients, uint8_t *pixels, int width, int height, int coefficientsPerRow, int bytesPerRow, NSData *idctData) {
|
||||
IFAST_MULT_TYPE *ifmtbl = (IFAST_MULT_TYPE *)idctData.bytes;
|
||||
|
||||
DCTELEM coefficientBlock[DCTSIZE2];
|
||||
JSAMPLE pixelBlock[DCTSIZE2];
|
||||
|
||||
for (int y = 0; y < height; y += DCTSIZE) {
|
||||
for (int x = 0; x < width; x += DCTSIZE) {
|
||||
for (int blockY = 0; blockY < DCTSIZE; blockY++) {
|
||||
for (int blockX = 0; blockX < DCTSIZE; blockX++) {
|
||||
coefficientBlock[zigZag[blockY * DCTSIZE + blockX]] = coefficients[(y + blockY) * coefficientsPerRow + (x + blockX)];
|
||||
}
|
||||
}
|
||||
|
||||
jsimd_idct_ifast_neon(ifmtbl, coefficientBlock, pixelBlock);
|
||||
|
||||
for (int blockY = 0; blockY < DCTSIZE; blockY++) {
|
||||
for (int blockX = 0; blockX < DCTSIZE; blockX++) {
|
||||
pixels[(y + blockY) * bytesPerRow + (x + blockX)] = pixelBlock[blockY * DCTSIZE + blockX];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
#import <DCT/YuvConversion.h>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Accelerate/Accelerate.h>
|
||||
|
||||
static uint8_t permuteMap[4] = { 3, 2, 1, 0};
|
||||
|
||||
void splitRGBAIntoYUVAPlanes(uint8_t const *argb, uint8_t *outY, uint8_t *outU, uint8_t *outV, uint8_t *outA, int width, int height, int bytesPerRow) {
|
||||
static vImage_ARGBToYpCbCr info;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
vImage_YpCbCrPixelRange pixelRange = (vImage_YpCbCrPixelRange){ 0, 128, 255, 255, 255, 1, 255, 0 };
|
||||
vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &pixelRange, &info, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, 0);
|
||||
});
|
||||
|
||||
vImage_Error error = kvImageNoError;
|
||||
|
||||
vImage_Buffer src;
|
||||
src.data = (void *)argb;
|
||||
src.width = width;
|
||||
src.height = height;
|
||||
src.rowBytes = bytesPerRow;
|
||||
|
||||
vImage_Buffer destYp;
|
||||
destYp.data = outY;
|
||||
destYp.width = width;
|
||||
destYp.height = height;
|
||||
destYp.rowBytes = width;
|
||||
|
||||
vImage_Buffer destCr;
|
||||
destCr.data = outU;
|
||||
destCr.width = width / 2;
|
||||
destCr.height = height / 2;
|
||||
destCr.rowBytes = width / 2;
|
||||
|
||||
vImage_Buffer destCb;
|
||||
destCb.data = outV;
|
||||
destCb.width = width / 2;
|
||||
destCb.height = height / 2;
|
||||
destCb.rowBytes = width / 2;
|
||||
|
||||
vImage_Buffer destA;
|
||||
destA.data = outA;
|
||||
destA.width = width;
|
||||
destA.height = height;
|
||||
destA.rowBytes = width;
|
||||
|
||||
error = vImageConvert_ARGB8888To420Yp8_Cb8_Cr8(&src, &destYp, &destCb, &destCr, &info, permuteMap, kvImageDoNotTile);
|
||||
if (error != kvImageNoError) {
|
||||
return;
|
||||
}
|
||||
|
||||
vImageExtractChannel_ARGB8888(&src, &destA, 3, kvImageDoNotTile);
|
||||
}
|
||||
|
||||
void combineYUVAPlanesIntoARBB(uint8_t *argb, uint8_t const *inY, uint8_t const *inU, uint8_t const *inV, uint8_t const *inA, int width, int height, int bytesPerRow) {
|
||||
static vImage_YpCbCrToARGB info;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
vImage_YpCbCrPixelRange pixelRange = (vImage_YpCbCrPixelRange){ 0, 128, 255, 255, 255, 1, 255, 0 };
|
||||
vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0);
|
||||
});
|
||||
|
||||
vImage_Error error = kvImageNoError;
|
||||
|
||||
vImage_Buffer destArgb;
|
||||
destArgb.data = (void *)argb;
|
||||
destArgb.width = width;
|
||||
destArgb.height = height;
|
||||
destArgb.rowBytes = bytesPerRow;
|
||||
|
||||
vImage_Buffer srcYp;
|
||||
srcYp.data = (void *)inY;
|
||||
srcYp.width = width;
|
||||
srcYp.height = height;
|
||||
srcYp.rowBytes = width;
|
||||
|
||||
vImage_Buffer srcCr;
|
||||
srcCr.data = (void *)inU;
|
||||
srcCr.width = width / 2;
|
||||
srcCr.height = height / 2;
|
||||
srcCr.rowBytes = width / 2;
|
||||
|
||||
vImage_Buffer srcCb;
|
||||
srcCb.data = (void *)inV;
|
||||
srcCb.width = width / 2;
|
||||
srcCb.height = height / 2;
|
||||
srcCb.rowBytes = width / 2;
|
||||
|
||||
vImage_Buffer srcA;
|
||||
srcA.data = (void *)inA;
|
||||
srcA.width = width;
|
||||
srcA.height = height;
|
||||
srcA.rowBytes = width;
|
||||
|
||||
error = vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&srcYp, &srcCb, &srcCr, &destArgb, &info, permuteMap, 255, kvImageDoNotTile);
|
||||
|
||||
error = vImageOverwriteChannels_ARGB8888(&srcA, &destArgb, &destArgb, 1 << 0, kvImageDoNotTile);
|
||||
}
|
||||
@ -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) {
|
||||
self.lock.locked {
|
||||
if self.isFailed {
|
||||
func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) {
|
||||
if self.isFailed || self.isFinished {
|
||||
return
|
||||
}
|
||||
|
||||
let parameterSet = ParameterSet(width: width, height: height, bytesPerRow: bytesPerRow)
|
||||
if let currentParameterSet = self.currentParameterSet {
|
||||
if currentParameterSet != parameterSet {
|
||||
self.lock.locked {
|
||||
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 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))
|
||||
if !self.isFailed {
|
||||
self.file = nil
|
||||
|
||||
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))
|
||||
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 { [weak writer] in
|
||||
if let writer = writer {
|
||||
writer.isCancelled = true
|
||||
}
|
||||
|
||||
itemContext.disposable.set(ActionDisposable {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,7 @@ swift_library(
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache",
|
||||
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
],
|
||||
|
||||
@ -11,25 +11,39 @@ import TelegramCore
|
||||
import Postbox
|
||||
import AnimationCache
|
||||
import LottieAnimationCache
|
||||
import VideoAnimationCache
|
||||
import MultiAnimationRenderer
|
||||
import ShimmerEffect
|
||||
import TextFormat
|
||||
|
||||
public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
public static let queue = Queue()
|
||||
|
||||
public struct Key: Hashable {
|
||||
public var id: MediaId
|
||||
public var id: Int64
|
||||
public var index: Int
|
||||
|
||||
public init(id: MediaId, index: Int) {
|
||||
public init(id: Int64, index: Int) {
|
||||
self.id = id
|
||||
self.index = index
|
||||
}
|
||||
}
|
||||
|
||||
private let file: TelegramMediaFile
|
||||
private let context: AccountContext
|
||||
private let groupId: String
|
||||
private let emoji: ChatTextInputTextCustomEmojiAttribute
|
||||
private let cache: AnimationCache
|
||||
private let renderer: MultiAnimationRenderer
|
||||
private let placeholderColor: UIColor
|
||||
|
||||
private let pointSize: CGSize
|
||||
private let pixelSize: CGSize
|
||||
|
||||
private var file: TelegramMediaFile?
|
||||
private var infoDisposable: Disposable?
|
||||
private var disposable: Disposable?
|
||||
private var fetchDisposable: Disposable?
|
||||
private var loadDisposable: Disposable?
|
||||
|
||||
private var isInHierarchyValue: Bool = false
|
||||
public var isVisibleForAnimations: Bool = false {
|
||||
@ -39,45 +53,36 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
}
|
||||
}
|
||||
}
|
||||
private var displayLink: ConstantDisplayLinkAnimator?
|
||||
|
||||
public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) {
|
||||
self.file = file
|
||||
public init(context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) {
|
||||
self.context = context
|
||||
self.groupId = groupId
|
||||
self.emoji = emoji
|
||||
self.cache = cache
|
||||
self.renderer = renderer
|
||||
self.placeholderColor = placeholderColor
|
||||
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
self.pointSize = CGSize(width: 24, height: 24)
|
||||
self.pixelSize = CGSize(width: self.pointSize.width * scale, height: self.pointSize.height * scale)
|
||||
|
||||
super.init()
|
||||
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
let pixelSize = CGSize(width: 24 * scale, height: 24 * scale)
|
||||
|
||||
if attemptSynchronousLoad {
|
||||
if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) {
|
||||
let size = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale)
|
||||
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) {
|
||||
self.contents = image.cgImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in
|
||||
let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
|
||||
|
||||
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
|
||||
guard let result = result else {
|
||||
self.infoDisposable = (context.engine.stickers.loadedStickerPack(reference: emoji.stickerPack, forceActualized: false)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else {
|
||||
writer.finish()
|
||||
return
|
||||
switch result {
|
||||
case let .result(_, items, _):
|
||||
for item in items {
|
||||
if item.file.fileId.id == emoji.fileId {
|
||||
strongSelf.updateFile(file: item.file, attemptSynchronousLoad: false)
|
||||
break
|
||||
}
|
||||
cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer)
|
||||
})
|
||||
|
||||
let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start()
|
||||
|
||||
return ActionDisposable {
|
||||
dataDisposable.dispose()
|
||||
fetchDisposable.dispose()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -91,6 +96,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.loadDisposable?.dispose()
|
||||
self.infoDisposable?.dispose()
|
||||
self.disposable?.dispose()
|
||||
self.fetchDisposable?.dispose()
|
||||
}
|
||||
@ -110,13 +117,70 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget {
|
||||
|
||||
self.shouldBeAnimating = shouldBePlaying
|
||||
}
|
||||
|
||||
private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) {
|
||||
if self.file?.fileId == file.fileId {
|
||||
return
|
||||
}
|
||||
|
||||
self.file = file
|
||||
|
||||
if attemptSynchronousLoad {
|
||||
if !self.renderer.loadFirstFrameSynchronously(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize) {
|
||||
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: self.placeholderColor) {
|
||||
self.contents = image.cgImage
|
||||
}
|
||||
}
|
||||
|
||||
self.loadAnimation()
|
||||
} else {
|
||||
self.loadDisposable = self.renderer.loadFirstFrame(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, completion: { [weak self] _ in
|
||||
self?.loadAnimation()
|
||||
})
|
||||
self.loadAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAnimation() {
|
||||
guard let file = self.file else {
|
||||
return
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in
|
||||
let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
|
||||
|
||||
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
|
||||
guard let result = result else {
|
||||
return
|
||||
}
|
||||
|
||||
if file.isVideoSticker {
|
||||
cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer)
|
||||
} else {
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else {
|
||||
writer.finish()
|
||||
return
|
||||
}
|
||||
cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer)
|
||||
}
|
||||
})
|
||||
|
||||
let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start()
|
||||
|
||||
return ActionDisposable {
|
||||
dataDisposable.dispose()
|
||||
fetchDisposable.dispose()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public final class EmojiTextAttachmentView: UIView {
|
||||
private let contentLayer: InlineStickerItemLayer
|
||||
|
||||
public init(context: AccountContext, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) {
|
||||
self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, file: file, cache: cache, renderer: renderer, placeholderColor: placeholderColor)
|
||||
public init(context: AccountContext, emoji: ChatTextInputTextCustomEmojiAttribute, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor) {
|
||||
self.contentLayer = InlineStickerItemLayer(context: context, groupId: "textInputView", attemptSynchronousLoad: true, emoji: emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor)
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
@ -131,6 +195,6 @@ public final class EmojiTextAttachmentView: UIView {
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.contentLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -2.0), size: CGSize(width: self.bounds.width - 0.0, height: self.bounds.height + 9.0))
|
||||
self.contentLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height))
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,17 +301,12 @@ public final class EmojiPagerContentComponent: Component {
|
||||
super.init()
|
||||
|
||||
if file.isAnimatedSticker || file.isVideoSticker {
|
||||
if attemptSynchronousLoad {
|
||||
if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) {
|
||||
self.displayPlaceholder = true
|
||||
|
||||
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) {
|
||||
self.contents = image.cgImage
|
||||
}
|
||||
}
|
||||
let loadAnimation: () -> Void = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in
|
||||
strongSelf.disposable = renderer.add(groupId: groupId, target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in
|
||||
let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false)
|
||||
|
||||
let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in
|
||||
@ -337,6 +332,23 @@ public final class EmojiPagerContentComponent: Component {
|
||||
fetchDisposable.dispose()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if attemptSynchronousLoad {
|
||||
if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) {
|
||||
self.displayPlaceholder = true
|
||||
|
||||
if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) {
|
||||
self.contents = image.cgImage
|
||||
}
|
||||
}
|
||||
|
||||
loadAnimation()
|
||||
} else {
|
||||
let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { _ in
|
||||
loadAnimation()
|
||||
})
|
||||
}
|
||||
} else if let dimensions = file.dimensions {
|
||||
let isSmall: Bool = false
|
||||
self.disposable = (chatMessageSticker(account: context.account, file: file, small: isSmall, synchronousLoad: attemptSynchronousLoad)).start(next: { [weak self] resultTransform in
|
||||
@ -588,7 +600,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
if let current = self.visibleItemLayers[itemId] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
itemLayer = ItemLayer(item: item, context: component.context, groupId: "keyboard", attemptSynchronousLoad: attemptSynchronousLoads, file: item.file, cache: component.animationCache, renderer: component.animationRenderer, placeholderColor: theme.chat.inputMediaPanel.stickersBackgroundColor, pointSize: CGSize(width: itemLayout.itemSize, height: itemLayout.itemSize))
|
||||
itemLayer = ItemLayer(item: item, context: component.context, groupId: "keyboard", attemptSynchronousLoad: attemptSynchronousLoads, file: item.file, cache: component.animationCache, renderer: component.animationRenderer, placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1), pointSize: CGSize(width: itemLayout.itemSize, height: itemLayout.itemSize))
|
||||
self.scrollView.layer.addSublayer(itemLayer)
|
||||
self.visibleItemLayers[itemId] = itemLayer
|
||||
}
|
||||
|
||||
@ -6,18 +6,23 @@ import RLottieBinding
|
||||
import GZip
|
||||
|
||||
public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: AnimationCacheItemWriter) {
|
||||
let decompressedData = TGGUnzipData(data, 512 * 1024) ?? data
|
||||
writer.queue.async {
|
||||
let decompressedData = TGGUnzipData(data, 1 * 1024 * 1024) ?? data
|
||||
guard let animation = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else {
|
||||
writer.finish()
|
||||
return
|
||||
}
|
||||
let size = CGSize(width: width, height: height)
|
||||
let context = DrawingContext(size: size, scale: 1.0, opaque: false, clear: true)
|
||||
|
||||
let frameDuration = 1.0 / Double(animation.frameRate)
|
||||
for i in 0 ..< animation.frameCount {
|
||||
animation.renderFrame(with: i, into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(context.scaledSize.width), height: Int32(context.scaledSize.height), bytesPerRow: Int32(context.bytesPerRow))
|
||||
writer.add(bytes: context.bytes, length: context.length, width: Int(context.scaledSize.width), height: Int(context.scaledSize.height), bytesPerRow: Int(context.bytesPerRow), duration: frameDuration)
|
||||
if writer.isCancelled {
|
||||
break
|
||||
}
|
||||
writer.add(with: { surface in
|
||||
animation.renderFrame(with: i, into: surface.argb, width: Int32(surface.width), height: Int32(surface.height), bytesPerRow: Int32(surface.bytesPerRow))
|
||||
}, proposedWidth: width, proposedHeight: height, duration: frameDuration)
|
||||
}
|
||||
|
||||
writer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import AnimationCache
|
||||
public protocol MultiAnimationRenderer: AnyObject {
|
||||
func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable
|
||||
func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool
|
||||
func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable
|
||||
}
|
||||
|
||||
open class MultiAnimationRenderTarget: SimpleLayer {
|
||||
@ -48,43 +49,19 @@ private func convertFrameToImage(frame: AnimationCacheItemFrame) -> UIImage? {
|
||||
private final class FrameGroup {
|
||||
let image: UIImage
|
||||
let size: CGSize
|
||||
let frameRange: Range<Int>
|
||||
let count: Int
|
||||
let skip: Int
|
||||
let timestamp: Double
|
||||
|
||||
init?(item: AnimationCacheItem, baseFrameIndex: Int, count: Int, skip: Int) {
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
assert(count % skip == 0)
|
||||
|
||||
let actualCount = count / skip
|
||||
|
||||
guard let firstFrame = item.getFrame(index: baseFrameIndex % item.numFrames) else {
|
||||
init?(item: AnimationCacheItem, timestamp: Double) {
|
||||
guard let firstFrame = item.getFrame(at: timestamp) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch firstFrame.format {
|
||||
case let .rgba(width, height, bytesPerRow):
|
||||
let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height * actualCount)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow)
|
||||
for i in stride(from: baseFrameIndex, to: baseFrameIndex + count, by: skip) {
|
||||
let frame: AnimationCacheItemFrame
|
||||
if i == baseFrameIndex {
|
||||
frame = firstFrame
|
||||
} else {
|
||||
if let nextFrame = item.getFrame(index: i % item.numFrames) {
|
||||
frame = nextFrame
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow)
|
||||
|
||||
let localFrameIndex = (i - baseFrameIndex) / skip
|
||||
|
||||
frame.data.withUnsafeBytes { bytes -> Void in
|
||||
memcpy(context.bytes.advanced(by: localFrameIndex * height * bytesPerRow), bytes.baseAddress!.advanced(by: frame.range.lowerBound), height * bytesPerRow)
|
||||
}
|
||||
firstFrame.data.withUnsafeBytes { bytes -> Void in
|
||||
memcpy(context.bytes, bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound), height * bytesPerRow)
|
||||
}
|
||||
|
||||
guard let image = context.generateImage() else {
|
||||
@ -93,22 +70,9 @@ private final class FrameGroup {
|
||||
|
||||
self.image = image
|
||||
self.size = CGSize(width: CGFloat(width), height: CGFloat(height))
|
||||
self.frameRange = baseFrameIndex ..< (baseFrameIndex + count)
|
||||
self.count = count
|
||||
self.skip = skip
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
|
||||
func contentsRect(index: Int) -> CGRect? {
|
||||
if !self.frameRange.contains(index) {
|
||||
return nil
|
||||
}
|
||||
let actualCount = self.count / self.skip
|
||||
let localIndex = (index - self.frameRange.lowerBound) / self.skip
|
||||
|
||||
let itemHeight = 1.0 / CGFloat(actualCount)
|
||||
return CGRect(origin: CGPoint(x: 0.0, y: CGFloat(localIndex) * itemHeight), size: CGSize(width: 1.0, height: itemHeight))
|
||||
}
|
||||
}
|
||||
|
||||
private final class LoadFrameGroupTask {
|
||||
@ -127,9 +91,8 @@ private final class ItemAnimationContext {
|
||||
|
||||
private var disposable: Disposable?
|
||||
private var displayLink: ConstantDisplayLinkAnimator?
|
||||
private var frameIndex: Int = 0
|
||||
private var timestamp: Double = 0.0
|
||||
private var item: AnimationCacheItem?
|
||||
private var frameSkip: Int
|
||||
|
||||
private var currentFrameGroup: FrameGroup?
|
||||
private var isLoadingFrameGroup: Bool = false
|
||||
@ -144,9 +107,8 @@ private final class ItemAnimationContext {
|
||||
|
||||
let targets = Bag<Weak<MultiAnimationRenderTarget>>()
|
||||
|
||||
init(cache: AnimationCache, itemId: String, size: CGSize, frameSkip: Int, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) {
|
||||
init(cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) {
|
||||
self.cache = cache
|
||||
self.frameSkip = frameSkip
|
||||
self.stateUpdated = stateUpdated
|
||||
|
||||
self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in
|
||||
@ -174,14 +136,9 @@ private final class ItemAnimationContext {
|
||||
}
|
||||
|
||||
func updateAddedTarget(target: MultiAnimationRenderTarget) {
|
||||
if let item = self.item, let currentFrameGroup = self.currentFrameGroup {
|
||||
let currentFrame = self.frameIndex % item.numFrames
|
||||
|
||||
if let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) {
|
||||
if let currentFrameGroup = self.currentFrameGroup {
|
||||
target.updateDisplayPlaceholder(displayPlaceholder: false)
|
||||
target.contents = currentFrameGroup.image.cgImage
|
||||
target.contentsRect = contentsRect
|
||||
}
|
||||
}
|
||||
|
||||
self.updateIsPlaying()
|
||||
@ -209,27 +166,26 @@ private final class ItemAnimationContext {
|
||||
self.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
func animationTick() -> LoadFrameGroupTask? {
|
||||
return self.update(advanceFrame: true)
|
||||
func animationTick(advanceTimestamp: Double) -> LoadFrameGroupTask? {
|
||||
return self.update(advanceTimestamp: advanceTimestamp)
|
||||
}
|
||||
|
||||
private func update(advanceFrame: Bool) -> LoadFrameGroupTask? {
|
||||
private func update(advanceTimestamp: Double?) -> LoadFrameGroupTask? {
|
||||
guard let item = self.item else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let currentFrame = self.frameIndex % item.numFrames
|
||||
let timestamp = self.timestamp
|
||||
if let advanceTimestamp = advanceTimestamp {
|
||||
self.timestamp += advanceTimestamp
|
||||
}
|
||||
|
||||
if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.frameRange.contains(currentFrame) {
|
||||
if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.timestamp == self.timestamp {
|
||||
} else if !self.isLoadingFrameGroup {
|
||||
self.currentFrameGroup = nil
|
||||
self.isLoadingFrameGroup = true
|
||||
let frameSkip = self.frameSkip
|
||||
|
||||
return LoadFrameGroupTask(task: { [weak self] in
|
||||
let possibleCounts: [Int] = [10, 12, 14, 16, 18, 20]
|
||||
let countIndex = Int.random(in: 0 ..< possibleCounts.count)
|
||||
let currentFrameGroup = FrameGroup(item: item, baseFrameIndex: currentFrame, count: possibleCounts[countIndex], skip: frameSkip)
|
||||
let currentFrameGroup = FrameGroup(item: item, timestamp: timestamp)
|
||||
|
||||
return {
|
||||
guard let strongSelf = self else {
|
||||
@ -241,24 +197,20 @@ private final class ItemAnimationContext {
|
||||
if let currentFrameGroup = currentFrameGroup {
|
||||
strongSelf.currentFrameGroup = currentFrameGroup
|
||||
for target in strongSelf.targets.copyItems() {
|
||||
target.value?.contents = currentFrameGroup.image.cgImage
|
||||
if let target = target.value {
|
||||
target.contents = currentFrameGroup.image.cgImage
|
||||
target.updateDisplayPlaceholder(displayPlaceholder: false)
|
||||
}
|
||||
}
|
||||
|
||||
let _ = strongSelf.update(advanceFrame: false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if advanceFrame {
|
||||
self.frameIndex += self.frameSkip
|
||||
}
|
||||
|
||||
if let currentFrameGroup = self.currentFrameGroup, let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) {
|
||||
if let _ = self.currentFrameGroup {
|
||||
for target in self.targets.copyItems() {
|
||||
if let target = target.value {
|
||||
target.updateDisplayPlaceholder(displayPlaceholder: false)
|
||||
target.contentsRect = contentsRect
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -269,7 +221,7 @@ private final class ItemAnimationContext {
|
||||
|
||||
public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
private final class GroupContext {
|
||||
private var frameSkip: Int
|
||||
private let firstFrameQueue: Queue
|
||||
private let stateUpdated: () -> Void
|
||||
|
||||
private var itemContexts: [String: ItemAnimationContext] = [:]
|
||||
@ -282,8 +234,8 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
init(frameSkip: Int, stateUpdated: @escaping () -> Void) {
|
||||
self.frameSkip = frameSkip
|
||||
init(firstFrameQueue: Queue, stateUpdated: @escaping () -> Void) {
|
||||
self.firstFrameQueue = firstFrameQueue
|
||||
self.stateUpdated = stateUpdated
|
||||
}
|
||||
|
||||
@ -292,7 +244,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
if let current = self.itemContexts[itemId] {
|
||||
itemContext = current
|
||||
} else {
|
||||
itemContext = ItemAnimationContext(cache: cache, itemId: itemId, size: size, frameSkip: self.frameSkip, fetch: fetch, stateUpdated: { [weak self] in
|
||||
itemContext = ItemAnimationContext(cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -339,8 +291,8 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
}
|
||||
|
||||
func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool {
|
||||
if let item = cache.getSynchronously(sourceId: itemId, size: size) {
|
||||
guard let frameGroup = FrameGroup(item: item, baseFrameIndex: 0, count: 1, skip: 1) else {
|
||||
if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) {
|
||||
guard let frameGroup = FrameGroup(item: item, timestamp: 0.0) else {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -352,6 +304,33 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable {
|
||||
return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, completion: { [weak target] item in
|
||||
guard let item = item else {
|
||||
Queue.mainQueue().async {
|
||||
completion(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let frameGroup = FrameGroup(item: item, timestamp: 0.0)
|
||||
|
||||
Queue.mainQueue().async {
|
||||
guard let target = target else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
if let frameGroup = frameGroup {
|
||||
target.contents = frameGroup.image.cgImage
|
||||
|
||||
completion(true)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func updateIsPlaying() {
|
||||
var isPlaying = false
|
||||
for (_, itemContext) in self.itemContexts {
|
||||
@ -364,11 +343,11 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
self.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
func animationTick() -> [LoadFrameGroupTask] {
|
||||
func animationTick(advanceTimestamp: Double) -> [LoadFrameGroupTask] {
|
||||
var tasks: [LoadFrameGroupTask] = []
|
||||
for (_, itemContext) in self.itemContexts {
|
||||
if itemContext.isPlaying {
|
||||
if let task = itemContext.animationTick() {
|
||||
if let task = itemContext.animationTick(advanceTimestamp: advanceTimestamp) {
|
||||
tasks.append(task)
|
||||
}
|
||||
}
|
||||
@ -378,6 +357,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
private let firstFrameQueue: Queue
|
||||
private var groupContexts: [String: GroupContext] = [:]
|
||||
private var frameSkip: Int
|
||||
private var displayLink: ConstantDisplayLinkAnimator?
|
||||
@ -407,6 +387,8 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive)
|
||||
|
||||
if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 {
|
||||
self.frameSkip = 1
|
||||
} else {
|
||||
@ -419,7 +401,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
if let current = self.groupContexts[groupId] {
|
||||
groupContext = current
|
||||
} else {
|
||||
groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in
|
||||
groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -440,7 +422,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
if let current = self.groupContexts[groupId] {
|
||||
groupContext = current
|
||||
} else {
|
||||
groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in
|
||||
groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -452,6 +434,23 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size)
|
||||
}
|
||||
|
||||
public func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable {
|
||||
let groupContext: GroupContext
|
||||
if let current = self.groupContexts[groupId] {
|
||||
groupContext = current
|
||||
} else {
|
||||
groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateIsPlaying()
|
||||
})
|
||||
self.groupContexts[groupId] = groupContext
|
||||
}
|
||||
|
||||
return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, completion: completion)
|
||||
}
|
||||
|
||||
private func updateIsPlaying() {
|
||||
var isPlaying = false
|
||||
for (_, groupContext) in self.groupContexts {
|
||||
@ -464,11 +463,20 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer {
|
||||
self.isPlaying = isPlaying
|
||||
}
|
||||
|
||||
private var previousTimestamp: Double?
|
||||
|
||||
private func animationTick() {
|
||||
let timestamp = CFAbsoluteTimeGetCurrent()
|
||||
if let _ = self.previousTimestamp {
|
||||
}
|
||||
self.previousTimestamp = timestamp
|
||||
|
||||
let secondsPerFrame = Double(self.frameSkip) / 60.0
|
||||
|
||||
var tasks: [LoadFrameGroupTask] = []
|
||||
for (_, groupContext) in self.groupContexts {
|
||||
if groupContext.isPlaying {
|
||||
tasks.append(contentsOf: groupContext.animationTick())
|
||||
tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,23 +5,46 @@ import Display
|
||||
import AnimatedStickerNode
|
||||
import SwiftSignalKit
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) {
|
||||
let queue = Queue()
|
||||
queue.async {
|
||||
guard let frameSource = makeVideoStickerDirectFrameSource(queue: queue, path: path, width: width, height: height, cachePathPrefix: nil) else {
|
||||
writer.queue.async {
|
||||
guard let frameSource = makeVideoStickerDirectFrameSource(queue: writer.queue, path: path, width: roundUp(width, multiple: 16), height: roundUp(height, multiple: 16), cachePathPrefix: nil) else {
|
||||
return
|
||||
}
|
||||
let frameDuration = 1.0 / Double(frameSource.frameRate)
|
||||
while true {
|
||||
if let frame = frameSource.takeFrame(draw: true) {
|
||||
//AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1, totalFrames: self.frameCount, multiplyAlpha: true)
|
||||
if case .argb = frame.type {
|
||||
let frameWidth = frame.width
|
||||
let frameHeight = frame.height
|
||||
let bytesPerRow = frame.bytesPerRow
|
||||
frame.data.withUnsafeBytes { bytes -> Void in
|
||||
writer.add(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), length: bytes.count, width: Int(frameWidth), height: Int(frameHeight), bytesPerRow: Int(bytesPerRow), duration: frameDuration)
|
||||
if writer.isCancelled {
|
||||
break
|
||||
}
|
||||
if let frame = frameSource.takeFrame(draw: true) {
|
||||
if case .argb = frame.type {
|
||||
let bytesPerRow = frame.bytesPerRow
|
||||
|
||||
writer.add(with: { surface in
|
||||
frame.data.withUnsafeBytes { bytes -> Void in
|
||||
let sourceArgb = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
if surface.bytesPerRow == bytesPerRow {
|
||||
memcpy(surface.argb, sourceArgb, min(surface.length, bytes.count))
|
||||
} else {
|
||||
let copyBytesPerRow = min(surface.bytesPerRow, bytesPerRow)
|
||||
for y in 0 ..< surface.height {
|
||||
memcpy(surface.argb.advanced(by: y * surface.bytesPerRow), sourceArgb.advanced(by: y * bytesPerRow), copyBytesPerRow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, proposedWidth: frame.width, proposedHeight: frame.height, duration: frameDuration)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
@ -8577,16 +8577,23 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
})
|
||||
}
|
||||
}, insertText: { [weak self] text in
|
||||
guard let strongSelf = self else {
|
||||
guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.chatDisplayNode.isTextInputPanelActive {
|
||||
return
|
||||
}
|
||||
guard let textInputPanelNode = strongSelf.chatDisplayNode.textInputPanelNode else {
|
||||
return
|
||||
|
||||
interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in
|
||||
let inputText = NSMutableAttributedString(attributedString: textInputState.inputText)
|
||||
|
||||
let range = textInputState.selectionRange
|
||||
inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: text)
|
||||
|
||||
let selectionPosition = range.lowerBound + (text.string as NSString).length
|
||||
|
||||
return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode)
|
||||
}
|
||||
textInputPanelNode.insertText(string: text)
|
||||
}, backwardsDeleteText: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
||||
@ -13,6 +13,7 @@ import Postbox
|
||||
import TelegramCore
|
||||
import ComponentDisplayAdapters
|
||||
import SettingsUI
|
||||
import TextFormat
|
||||
|
||||
final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
struct InputData: Equatable {
|
||||
@ -40,7 +41,24 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
guard let interfaceInteraction = interfaceInteraction else {
|
||||
return
|
||||
}
|
||||
interfaceInteraction.insertText(item.emoji)
|
||||
var text = "."
|
||||
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
||||
loop: for attribute in item.file.attributes {
|
||||
switch attribute {
|
||||
case let .Sticker(displayText, packReference, _):
|
||||
text = displayText
|
||||
if let packReference = packReference {
|
||||
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: item.file.fileId.id)
|
||||
break loop
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let emojiAttribute = emojiAttribute {
|
||||
interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
|
||||
}
|
||||
},
|
||||
deleteBackwards: { [weak interfaceInteraction] in
|
||||
guard let interfaceInteraction = interfaceInteraction else {
|
||||
@ -87,38 +105,34 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
})
|
||||
let animationRenderer = MultiAnimationRendererImpl()
|
||||
|
||||
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
|
||||
context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false),
|
||||
context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers))
|
||||
)
|
||||
|> map { animatedEmoji, savedStickers -> EmojiPagerContentComponent in
|
||||
let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
|
||||
let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
|
||||
|
||||
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000)
|
||||
|> map { view -> EmojiPagerContentComponent in
|
||||
var emojiItems: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
for item in savedStickers {
|
||||
if let item = item.contents.get(SavedStickerItem.self) {
|
||||
if item.file.isVideoSticker {
|
||||
emojiItems.append(EmojiPagerContentComponent.Item(
|
||||
emoji: "",
|
||||
file: item.file
|
||||
))
|
||||
var emojiCollectionIds = Set<ItemCollectionId>()
|
||||
for (id, info, _) in view.collectionInfos {
|
||||
if let info = info as? StickerPackCollectionInfo {
|
||||
if info.shortName.lowercased().contains("emoji") {
|
||||
emojiCollectionIds.insert(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch animatedEmoji {
|
||||
case let .result(_, items, _):
|
||||
for item in items {
|
||||
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
|
||||
let strippedEmoji = emoji.basicEmoji.0.strippedEmoji
|
||||
emojiItems.append(EmojiPagerContentComponent.Item(
|
||||
emoji: strippedEmoji,
|
||||
for entry in view.entries {
|
||||
guard let item = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
if emojiCollectionIds.contains(entry.index.collectionId) {
|
||||
let resultItem = EmojiPagerContentComponent.Item(
|
||||
emoji: "",
|
||||
file: item.file
|
||||
))
|
||||
)
|
||||
emojiItems.append(resultItem)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
var itemGroups: [EmojiPagerContentComponent.ItemGroup] = []
|
||||
itemGroups.append(EmojiPagerContentComponent.ItemGroup(
|
||||
@ -145,8 +159,6 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
|
||||
let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
|
||||
let stickerItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000),
|
||||
hasPremium
|
||||
|
||||
@ -47,18 +47,18 @@ private final class CachedChatMessageText {
|
||||
}
|
||||
|
||||
private final class InlineStickerItem: Hashable {
|
||||
let file: TelegramMediaFile
|
||||
let emoji: ChatTextInputTextCustomEmojiAttribute
|
||||
|
||||
init(file: TelegramMediaFile) {
|
||||
self.file = file
|
||||
init(emoji: ChatTextInputTextCustomEmojiAttribute) {
|
||||
self.emoji = emoji
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(self.file.fileId)
|
||||
hasher.combine(emoji.fileId)
|
||||
}
|
||||
|
||||
static func ==(lhs: InlineStickerItem, rhs: InlineStickerItem) -> Bool {
|
||||
if lhs.file.fileId != rhs.file.fileId {
|
||||
if lhs.emoji.fileId != rhs.emoji.fileId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -341,38 +341,26 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
attributedText = NSAttributedString(string: " ", font: textFont, textColor: messageTheme.primaryTextColor)
|
||||
}
|
||||
|
||||
/*if let entities = entities {
|
||||
if let entities = entities {
|
||||
let updatedString = NSMutableAttributedString(attributedString: attributedText)
|
||||
|
||||
for entity in entities.sorted(by: { $0.range.lowerBound > $1.range.lowerBound }) {
|
||||
guard case .AnimatedEmoji = entity.type else {
|
||||
guard case let .CustomEmoji(stickerPack, fileId) = entity.type else {
|
||||
continue
|
||||
}
|
||||
|
||||
let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
|
||||
let substring = updatedString.attributedSubstring(from: range)
|
||||
|
||||
let emoji = substring.string.basicEmoji.0
|
||||
|
||||
var emojiFile: TelegramMediaFile?
|
||||
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
|
||||
if emojiFile == nil {
|
||||
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
|
||||
}
|
||||
|
||||
if let emojiFile = emojiFile {
|
||||
let currentDict = updatedString.attributes(at: range.lowerBound, effectiveRange: nil)
|
||||
var updatedAttributes: [NSAttributedString.Key: Any] = currentDict
|
||||
updatedAttributes[NSAttributedString.Key.foregroundColor] = UIColor.clear.cgColor
|
||||
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(file: emojiFile)
|
||||
updatedAttributes[NSAttributedString.Key("Attribute__EmbeddedItem")] = InlineStickerItem(emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId))
|
||||
|
||||
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}]", attributes: updatedAttributes)
|
||||
let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}\u{00a0}]", attributes: updatedAttributes)
|
||||
updatedString.replaceCharacters(in: range, with: insertString)
|
||||
}
|
||||
}
|
||||
attributedText = updatedString
|
||||
}*/
|
||||
}
|
||||
|
||||
let cutout: TextNodeCutout? = nil
|
||||
|
||||
@ -558,27 +546,27 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) {
|
||||
var nextIndexById: [MediaId: Int] = [:]
|
||||
var nextIndexById: [Int64: Int] = [:]
|
||||
var validIds: [InlineStickerItemLayer.Key] = []
|
||||
|
||||
if let textLayout = textLayout {
|
||||
for item in textLayout.embeddedItems {
|
||||
if let stickerItem = item.value as? InlineStickerItem {
|
||||
let index: Int
|
||||
if let currentNext = nextIndexById[stickerItem.file.fileId] {
|
||||
if let currentNext = nextIndexById[stickerItem.emoji.fileId] {
|
||||
index = currentNext
|
||||
} else {
|
||||
index = 0
|
||||
}
|
||||
nextIndexById[stickerItem.file.fileId] = index + 1
|
||||
let id = InlineStickerItemLayer.Key(id: stickerItem.file.fileId, index: index)
|
||||
nextIndexById[stickerItem.emoji.fileId] = index + 1
|
||||
let id = InlineStickerItemLayer.Key(id: stickerItem.emoji.fileId, index: index)
|
||||
validIds.append(id)
|
||||
|
||||
let itemLayer: InlineStickerItemLayer
|
||||
if let current = self.inlineStickerItemLayers[id] {
|
||||
itemLayer = current
|
||||
} else {
|
||||
itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor)
|
||||
itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, cache: cache, renderer: renderer, placeholderColor: placeholderColor)
|
||||
self.inlineStickerItemLayers[id] = itemLayer
|
||||
self.textNode.layer.addSublayer(itemLayer)
|
||||
itemLayer.isVisibleForAnimations = self.isVisibleForAnimations
|
||||
|
||||
@ -244,6 +244,67 @@ enum ChatTextInputPanelPasteData {
|
||||
case sticker(UIImage, Bool)
|
||||
}
|
||||
|
||||
final class CustomEmojiContainerView: UIView {
|
||||
private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView?
|
||||
|
||||
private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:]
|
||||
|
||||
init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) {
|
||||
self.emojiViewProvider = emojiViewProvider
|
||||
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) {
|
||||
var nextIndexById: [Int64: Int] = [:]
|
||||
|
||||
var validKeys = Set<InlineStickerItemLayer.Key>()
|
||||
for (rect, emoji) in emojiRects {
|
||||
let index: Int
|
||||
if let nextIndex = nextIndexById[emoji.fileId] {
|
||||
index = nextIndex
|
||||
} else {
|
||||
index = 0
|
||||
}
|
||||
nextIndexById[emoji.fileId] = index + 1
|
||||
|
||||
let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index)
|
||||
|
||||
let view: UIView
|
||||
if let current = self.emojiLayers[key] {
|
||||
view = current
|
||||
} else if let newView = self.emojiViewProvider(emoji) {
|
||||
view = newView
|
||||
self.addSubview(newView)
|
||||
self.emojiLayers[key] = view
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
let size = CGSize(width: 24.0, height: 24.0)
|
||||
|
||||
view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0)), size: size)
|
||||
|
||||
validKeys.insert(key)
|
||||
}
|
||||
|
||||
var removeKeys: [InlineStickerItemLayer.Key] = []
|
||||
for (key, view) in self.emojiLayers {
|
||||
if !validKeys.contains(key) {
|
||||
removeKeys.append(key)
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
for key in removeKeys {
|
||||
self.emojiLayers.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let clippingNode: ASDisplayNode
|
||||
var textPlaceholderNode: ImmediateTextNode
|
||||
@ -253,6 +314,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let textInputContainer: ASDisplayNode
|
||||
var textInputNode: EditableTextNode?
|
||||
var dustNode: InvisibleInkDustNode?
|
||||
var customEmojiContainerView: CustomEmojiContainerView?
|
||||
|
||||
let textInputBackgroundNode: ASImageNode
|
||||
private var transparentTextInputBackgroundImage: UIImage?
|
||||
@ -463,7 +525,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
private var touchDownGestureRecognizer: TouchDownGestureRecognizer?
|
||||
|
||||
private var emojiViewProvider: ((String) -> UIView)?
|
||||
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
|
||||
|
||||
init(presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) {
|
||||
self.presentationInterfaceState = presentationInterfaceState
|
||||
@ -673,11 +735,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
|
||||
if let presentationContext = presentationContext {
|
||||
self.emojiViewProvider = { [weak self, weak presentationContext] emoji in
|
||||
guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context, let file = strongSelf.context?.animatedEmojiStickers[emoji]?.first?.file else {
|
||||
guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context else {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
return EmojiTextAttachmentView(context: context, file: file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12))
|
||||
return EmojiTextAttachmentView(context: context, emoji: emoji, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1904,6 +1966,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor
|
||||
|
||||
var rects: [CGRect] = []
|
||||
var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []
|
||||
|
||||
if let attributedText = textInputNode.attributedText {
|
||||
let beginning = textInputNode.textView.beginningOfDocument
|
||||
@ -1940,6 +2003,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let endIndex = currentIndex
|
||||
addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
|
||||
}
|
||||
} else if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) {
|
||||
let textRects = textInputNode.textView.selectionRects(for: textRange)
|
||||
for textRect in textRects {
|
||||
customEmojiRects.append((textRect.rect, value))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1961,6 +2032,28 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
dustNode.removeFromSupernode()
|
||||
self.dustNode = nil
|
||||
}
|
||||
|
||||
if !customEmojiRects.isEmpty {
|
||||
let customEmojiContainerView: CustomEmojiContainerView
|
||||
if let current = self.customEmojiContainerView {
|
||||
customEmojiContainerView = current
|
||||
} else {
|
||||
customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
|
||||
guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
|
||||
return nil
|
||||
}
|
||||
return emojiViewProvider(emoji)
|
||||
})
|
||||
customEmojiContainerView.isUserInteractionEnabled = false
|
||||
textInputNode.textView.addSubview(customEmojiContainerView)
|
||||
self.customEmojiContainerView = customEmojiContainerView
|
||||
}
|
||||
|
||||
customEmojiContainerView.update(emojiRects: customEmojiRects)
|
||||
} else if let customEmojiContainerView = self.customEmojiContainerView {
|
||||
customEmojiContainerView.removeFromSuperview()
|
||||
self.customEmojiContainerView = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSpoilersRevealed(animated: Bool = true) {
|
||||
@ -2226,7 +2319,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if mediaInputIsActive {
|
||||
if mediaInputIsActive && !"".isEmpty {
|
||||
if self.actionButtons.expandMediaInputButton.alpha.isZero {
|
||||
self.actionButtons.expandMediaInputButton.alpha = 1.0
|
||||
if animated {
|
||||
@ -2727,13 +2820,6 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
self.textInputNode?.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func insertText(string: String) {
|
||||
guard let textInputNode = self.textInputNode else {
|
||||
return
|
||||
}
|
||||
textInputNode.textView.insertText(string)
|
||||
}
|
||||
|
||||
func backwardsDeleteText() {
|
||||
guard let textInputNode = self.textInputNode else {
|
||||
return
|
||||
|
||||
@ -3,6 +3,7 @@ import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import Emoji
|
||||
|
||||
@ -17,8 +18,9 @@ public struct ChatTextInputAttributes {
|
||||
public static let textMention = NSAttributedString.Key(rawValue: "Attribute__TextMention")
|
||||
public static let textUrl = NSAttributedString.Key(rawValue: "Attribute__TextUrl")
|
||||
public static let spoiler = NSAttributedString.Key(rawValue: "Attribute__Spoiler")
|
||||
public static let customEmoji = NSAttributedString.Key(rawValue: "Attribute__CustomEmoji")
|
||||
|
||||
public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler]
|
||||
public static let allAttributes = [ChatTextInputAttributes.bold, ChatTextInputAttributes.italic, ChatTextInputAttributes.monospace, ChatTextInputAttributes.strikethrough, ChatTextInputAttributes.underline, ChatTextInputAttributes.textMention, ChatTextInputAttributes.textUrl, ChatTextInputAttributes.spoiler, ChatTextInputAttributes.customEmoji]
|
||||
}
|
||||
|
||||
public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttributedString {
|
||||
@ -28,7 +30,7 @@ public func stateAttributedStringForText(_ text: NSAttributedString) -> NSAttrib
|
||||
let fullRange = NSRange(sourceString.string.startIndex ..< sourceString.string.endIndex, in: sourceString.string)
|
||||
sourceString.enumerateAttribute(NSAttributedString.Key.attachment, in: fullRange, options: [.longestEffectiveRangeNotRequired], using: { value, range, stop in
|
||||
if let value = value as? EmojiTextAttachment {
|
||||
sourceString.replaceCharacters(in: range, with: NSAttributedString(string: value.emoji))
|
||||
sourceString.replaceCharacters(in: range, with: NSAttributedString(string: value.text, attributes: [ChatTextInputAttributes.customEmoji: value.emoji]))
|
||||
stop.pointee = true
|
||||
found = true
|
||||
}
|
||||
@ -64,7 +66,7 @@ public struct ChatTextFontAttributes: OptionSet {
|
||||
public static let blockQuote = ChatTextFontAttributes(rawValue: 1 << 3)
|
||||
}
|
||||
|
||||
public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool, availableEmojis: Set<String>, emojiViewProvider: ((String) -> UIView)?) -> NSAttributedString {
|
||||
public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool, availableEmojis: Set<String>, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) -> NSAttributedString {
|
||||
let result = NSMutableAttributedString(string: stateText.string)
|
||||
let fullRange = NSRange(location: 0, length: result.length)
|
||||
|
||||
@ -108,6 +110,8 @@ public func textAttributedStringForStateText(_ stateText: NSAttributedString, fo
|
||||
} else {
|
||||
result.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
||||
}
|
||||
} else if key == ChatTextInputAttributes.customEmoji {
|
||||
result.addAttribute(key, value: value, range: range)
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,6 +219,26 @@ public final class ChatTextInputTextUrlAttribute: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatTextInputTextCustomEmojiAttribute: NSObject {
|
||||
public let stickerPack: StickerPackReference
|
||||
public let fileId: Int64
|
||||
|
||||
public init(stickerPack: StickerPackReference, fileId: Int64) {
|
||||
self.stickerPack = stickerPack
|
||||
self.fileId = fileId
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public func isEqual(_ object: Any?) -> Bool {
|
||||
if let other = object as? ChatTextInputTextCustomEmojiAttribute {
|
||||
return self.stickerPack == other.stickerPack && self.fileId == other.fileId
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func textUrlRangesEqual(_ lhs: [(NSRange, ChatTextInputTextUrlAttribute)], _ rhs: [(NSRange, ChatTextInputTextUrlAttribute)]) -> Bool {
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
@ -459,7 +483,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed
|
||||
}
|
||||
}
|
||||
|
||||
public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set<String>, emojiViewProvider: ((String) -> UIView)?) {
|
||||
public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set<String>, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?) {
|
||||
guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else {
|
||||
return
|
||||
}
|
||||
@ -493,10 +517,14 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
|
||||
textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.textMention, range: fullRange)
|
||||
textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.textUrl, range: fullRange)
|
||||
textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.spoiler, range: fullRange)
|
||||
textNode.textView.textStorage.removeAttribute(ChatTextInputAttributes.customEmoji, range: fullRange)
|
||||
|
||||
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.font, value: Font.regular(baseFontSize), range: fullRange)
|
||||
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.chat.inputPanel.primaryTextColor, range: fullRange)
|
||||
|
||||
let replaceRanges: [(NSRange, EmojiTextAttachment)] = []
|
||||
|
||||
//var emojiIndex = 0
|
||||
attributedText.enumerateAttributes(in: fullRange, options: [], using: { attributes, range, _ in
|
||||
var fontAttributes: ChatTextFontAttributes = []
|
||||
|
||||
@ -530,6 +558,17 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
|
||||
} else {
|
||||
textNode.textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range)
|
||||
}
|
||||
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||
textNode.textView.textStorage.addAttribute(key, value: value, range: range)
|
||||
if let emojiViewProvider = emojiViewProvider {
|
||||
let _ = emojiViewProvider
|
||||
/*let emojiText = attributedText.attributedSubstring(from: range)
|
||||
let attachment = EmojiTextAttachment(index: emojiIndex, text: emojiText.string, emoji: value, viewProvider: emojiViewProvider)
|
||||
emojiIndex += 1
|
||||
attachment.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 16.0))
|
||||
|
||||
replaceRanges.append((range, attachment))*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -556,12 +595,16 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (range, attachment) in replaceRanges.sorted(by: { $0.0.location > $1.0.location }) {
|
||||
textNode.textView.textStorage.replaceCharacters(in: range, with: NSAttributedString(attachment: attachment))
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 15, *), let emojiViewProvider = emojiViewProvider {
|
||||
if #available(iOS 15, *), let _ = emojiViewProvider {
|
||||
let _ = CustomTextAttachmentViewProvider.ensureRegistered
|
||||
|
||||
var nextIndex: [String: Int] = [:]
|
||||
/*var nextIndex: [String: Int] = [:]
|
||||
|
||||
var count = 0
|
||||
|
||||
@ -572,7 +615,7 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
|
||||
}
|
||||
})
|
||||
|
||||
while count < 10 {
|
||||
while count < 400 {
|
||||
var found = false
|
||||
textNode.textView.textStorage.string.enumerateSubstrings(in: textNode.textView.textStorage.string.startIndex ..< textNode.textView.textStorage.string.endIndex, options: [.byComposedCharacterSequences]) { substring, substringRange, _, stop in
|
||||
if let substring = substring {
|
||||
@ -601,11 +644,11 @@ public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set<String>, emojiViewProvider: ((String) -> UIView)?, spoilersRevealed: Bool = false) {
|
||||
public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set<String>, emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?, spoilersRevealed: Bool = false) {
|
||||
guard let initialAttributedText = textNode.attributedText, initialAttributedText.length != 0 else {
|
||||
return
|
||||
}
|
||||
@ -902,10 +945,12 @@ public func convertMarkdownToAttributes(_ text: NSAttributedString) -> NSAttribu
|
||||
}
|
||||
|
||||
private final class EmojiTextAttachment: NSTextAttachment {
|
||||
let emoji: String
|
||||
let viewProvider: (String) -> UIView
|
||||
let text: String
|
||||
let emoji: ChatTextInputTextCustomEmojiAttribute
|
||||
let viewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView
|
||||
|
||||
init(index: Int, emoji: String, viewProvider: @escaping (String) -> UIView) {
|
||||
init(index: Int, text: String, emoji: ChatTextInputTextCustomEmojiAttribute, viewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView) {
|
||||
self.text = text
|
||||
self.emoji = emoji
|
||||
self.viewProvider = viewProvider
|
||||
|
||||
|
||||
@ -146,26 +146,6 @@ private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType,
|
||||
public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimatedEmojisInText: Int? = nil) -> [MessageTextEntity] {
|
||||
var entities: [MessageTextEntity] = []
|
||||
|
||||
if let maxAnimatedEmojisInText = maxAnimatedEmojisInText, maxAnimatedEmojisInText != 0 {
|
||||
var count = 0
|
||||
text.string.enumerateSubstrings(in: text.string.startIndex ..< text.string.endIndex, options: [.byComposedCharacterSequences], { substring, substringRange, _, stop in
|
||||
if let substring = substring {
|
||||
let emoji = substring.basicEmoji.0
|
||||
|
||||
if !emoji.isEmpty && emoji.isSingleEmoji {
|
||||
let mappedRange = NSRange(substringRange, in: text.string)
|
||||
|
||||
entities.append(MessageTextEntity(range: mappedRange.lowerBound ..< mappedRange.upperBound, type: .AnimatedEmoji(nil)))
|
||||
|
||||
count += 1
|
||||
if count >= maxAnimatedEmojisInText {
|
||||
stop = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: [], using: { attributes, range, _ in
|
||||
for (key, value) in attributes {
|
||||
if key == ChatTextInputAttributes.bold {
|
||||
@ -184,6 +164,8 @@ public func generateChatInputTextEntities(_ text: NSAttributedString, maxAnimate
|
||||
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .TextUrl(url: value.url)))
|
||||
} else if key == ChatTextInputAttributes.spoiler {
|
||||
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Spoiler))
|
||||
} else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .CustomEmoji(stickerPack: value.stickerPack, fileId: value.fileId)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -40,6 +40,8 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M
|
||||
string.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: range)
|
||||
case .Spoiler:
|
||||
string.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: range)
|
||||
case let .CustomEmoji(stickerPack, fileId):
|
||||
string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId), range: range)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user