diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 63a69e990c..87464bb853 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -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 diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm index 7bf26bcf8b..7bfee153fa 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm @@ -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 *)attributes inContext:(CGContextRef)graphicsContext { + for (NSUInteger i = 0; i < glyphCount; i++) { + if (attributes[@"Attribute__CustomEmoji"] != nil) { + continue; + } + + [super showCGGlyphs:&glyphs[i] positions:&positions[i] count:1 font:font matrix:textMatrix attributes:attributes inContext:graphicsContext]; } - - CGRect bounds = [self boundingRectForGlyphRange:glyphsToShow inTextContainer:[self textContainerForGlyphAtIndex:glyphsToShow.location effectiveRange:nil]]; - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextSetFillColorWithColor(context, [UIColor grayColor].CGColor); - CGContextFillRect(context, bounds);*/ - +} + +- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin { [super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin]; } diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 43a96c5d10..c0afa5705a 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -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? diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index cae532aea0..89803b3707 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -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? diff --git a/submodules/ManagedFile/Sources/ManagedFile.swift b/submodules/ManagedFile/Sources/ManagedFile.swift index d43ab46369..ba5c4a5cbf 100644 --- a/submodules/ManagedFile/Sources/ManagedFile.swift +++ b/submodules/ManagedFile/Sources/ManagedFile.swift @@ -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 { @@ -48,16 +49,27 @@ public final class ManagedFile { } deinit { + 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) } } diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 5a6a01015f..f1cbdb1419 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -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 { diff --git a/submodules/Postbox/Sources/MediaBoxFile.swift b/submodules/Postbox/Sources/MediaBoxFile.swift index 578f1a82b2..aeb69d5829 100644 --- a/submodules/Postbox/Sources/MediaBoxFile.swift +++ b/submodules/Postbox/Sources/MediaBoxFile.swift @@ -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 } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 4f77dde770..560e7fde82 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -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) } diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 8172159a5c..184426636f 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -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() diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index a9726d26a4..d18917198a 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift index 25da7187cb..6c3e4b7dcb 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift @@ -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 } diff --git a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift index 8cd7216db5..50b56a3a48 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index f0ea775946..dc83c6cd28 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -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! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index c01106354f..64a55fe83e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -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): diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift index 6906d5ba9f..7c6bbe7d3d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TextEntitiesMessageAttribute.swift @@ -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") diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 371cc76040..ccf6ae5604 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/AnimationCache/BUILD b/submodules/TelegramUI/Components/AnimationCache/BUILD index c5773d4849..3e79cc1f13 100644 --- a/submodules/TelegramUI/Components/AnimationCache/BUILD +++ b/submodules/TelegramUI/Components/AnimationCache/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/AnimationCache/DCT/BUILD b/submodules/TelegramUI/Components/AnimationCache/DCT/BUILD new file mode 100644 index 0000000000..f98db65bc2 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/DCT/BUILD @@ -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", + ], +) diff --git a/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/DCT.h b/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/DCT.h new file mode 100644 index 0000000000..01fa6dc26c --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/DCT.h @@ -0,0 +1,14 @@ +#ifndef DctImageTransform_h +#define DctImageTransform_h + +#import + +#import + +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 */ diff --git a/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/YuvConversion.h b/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/YuvConversion.h new file mode 100644 index 0000000000..a7ceddecb9 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/DCT/PublicHeaders/DCT/YuvConversion.h @@ -0,0 +1,9 @@ +#ifndef YuvConversion_h +#define YuvConversion_h + +#import + +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 */ diff --git a/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/DCT.m b/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/DCT.m new file mode 100644 index 0000000000..b01c769007 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/DCT.m @@ -0,0 +1,991 @@ +#import + +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 + +/* 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]; + } + } + } + } +} diff --git a/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/YuvConversion.m b/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/YuvConversion.m new file mode 100644 index 0000000000..25b244d5e1 --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/DCT/Sources/YuvConversion.m @@ -0,0 +1,99 @@ +#import + +#import +#import + +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); +} diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 3201000137..2f6e8197d6 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -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 + public let width: Int + public let height: Int + public let bytesPerRow: Int + public let length: Int + + init( + argb: UnsafeMutablePointer, + 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 - 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, decompressedSize: Int) -> Data? { + let algorithm: compression_algorithm = COMPRESSION_LZFSE + + let scratchData = malloc(compression_decode_scratch_buffer_size(algorithm))! + defer { + free(scratchData) + } + + var decompressedFrameData = Data(count: decompressedSize) + let resultSize = decompressedFrameData.withUnsafeMutableBytes { buffer -> Int in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return 0 + } + return data.withUnsafeBytes { sourceBuffer -> Int in + return compression_decode_buffer(bytes, buffer.count, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: range.lowerBound), range.upperBound - range.lowerBound, scratchData, algorithm) + } + } + + if resultSize <= 0 { + return nil + } + if decompressedFrameData.count != resultSize { + decompressedFrameData.count = resultSize + } + return decompressedFrameData +} + private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { - private struct ParameterSet: Equatable { - var width: Int - var height: Int - var bytesPerRow: Int + struct CompressedResult { + var animationPath: String + var firstFramePath: String } private struct FrameMetadata { @@ -93,10 +198,19 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { var duration: Double } - private let file: ManagedFile - private let completion: (Bool) -> Void + let queue: Queue + var isCancelled: Bool = false - private var currentParameterSet: ParameterSet? + private let decompressedPath: String + private let compressedPath: String + private let firstFramePath: String + private var file: ManagedFile? + private let completion: (CompressedResult?) -> Void + + private var currentSurface: ImageARGB? + private var currentYUVASurface: ImageYUVA420? + private var currentDctData: DctData? + private var currentDctCoefficients: DctCoefficientsYUVA420? private var contentLengthOffset: Int? private var isFailed: Bool = false private var isFinished: Bool = false @@ -104,44 +218,141 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { private var frames: [FrameMetadata] = [] private var contentLength: Int = 0 + private let dctQuality: Int + private let lock = Lock() - init?(tempPath: String, completion: @escaping (Bool) -> Void) { - guard let file = ManagedFile(queue: nil, path: tempPath, mode: .readwrite) else { + init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { + self.dctQuality = 67 + + self.queue = queue + self.decompressedPath = allocateTempFile() + self.compressedPath = allocateTempFile() + self.firstFramePath = allocateTempFile() + + guard let file = ManagedFile(queue: nil, path: self.decompressedPath, mode: .readwrite) else { return nil } self.file = file self.completion = completion } - func add(bytes: UnsafeRawPointer, length: Int, width: Int, height: Int, bytesPerRow: Int, duration: Double) { + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) { + if self.isFailed || self.isFinished { + return + } + self.lock.locked { - if self.isFailed { + guard !self.isFailed, !self.isFinished, let file = self.file else { return } - let parameterSet = ParameterSet(width: width, height: height, bytesPerRow: bytesPerRow) - if let currentParameterSet = self.currentParameterSet { - if currentParameterSet != parameterSet { + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedWidth, multiple: 16) + + var isFirstFrame = false + + let surface: ImageARGB + if let current = self.currentSurface { + if current.argbPlane.width == width && current.argbPlane.height == height { + surface = current + } else { self.isFailed = true return } } else { - self.currentParameterSet = parameterSet + isFirstFrame = true - self.file.write(1 as UInt32) - - self.file.write(UInt32(parameterSet.width)) - self.file.write(UInt32(parameterSet.height)) - self.file.write(UInt32(parameterSet.bytesPerRow)) - - self.contentLengthOffset = Int(self.file.position()) - self.file.write(0 as UInt32) + surface = ImageARGB(width: width, height: height) + self.currentSurface = surface } - self.frames.append(FrameMetadata(offset: Int(self.file.position()), length: length, duration: duration)) - let _ = self.file.write(bytes, count: length) - self.contentLength += length + let yuvaSurface: ImageYUVA420 + if let current = self.currentYUVASurface { + if current.yPlane.width == width && current.yPlane.height == height { + yuvaSurface = current + } else { + self.isFailed = true + return + } + } else { + yuvaSurface = ImageYUVA420(width: width, height: height) + self.currentYUVASurface = yuvaSurface + } + + let dctCoefficients: DctCoefficientsYUVA420 + if let current = self.currentDctCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + dctCoefficients = current + } else { + self.isFailed = true + return + } + } else { + dctCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.currentDctCoefficients = dctCoefficients + } + + let dctData: DctData + if let current = self.currentDctData, current.quality == self.dctQuality { + dctData = current + } else { + dctData = DctData(quality: self.dctQuality) + self.currentDctData = dctData + } + + surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Void in + drawingBlock(AnimationCacheItemDrawingSurface( + argb: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), + width: width, + height: height, + bytesPerRow: surface.argbPlane.bytesPerRow, + length: bytes.count + )) + } + + surface.toYUVA420(target: yuvaSurface) + yuvaSurface.dct(dctData: dctData, target: dctCoefficients) + + if isFirstFrame { + file.write(2 as UInt32) + + file.write(UInt32(dctCoefficients.yPlane.width)) + file.write(UInt32(dctCoefficients.yPlane.height)) + file.write(UInt32(dctData.quality)) + + self.contentLengthOffset = Int(file.position()) + file.write(0 as UInt32) + } + + let framePosition = Int(file.position()) + assert(framePosition >= 0) + var frameLength = 0 + + for i in 0 ..< 4 { + let dctPlane: DctCoefficientPlane + switch i { + case 0: + dctPlane = dctCoefficients.yPlane + case 1: + dctPlane = dctCoefficients.uPlane + case 2: + dctPlane = dctCoefficients.vPlane + case 3: + dctPlane = dctCoefficients.aPlane + default: + preconditionFailure() + } + + dctPlane.data.withUnsafeBytes { bytes in + let _ = file.write(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } + frameLength += dctPlane.data.count + } + + self.frames.append(FrameMetadata(offset: framePosition, length: frameLength, duration: duration)) + + self.contentLength += frameLength } } @@ -152,27 +363,96 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { self.isFinished = true shouldComplete = true - guard let contentLengthOffset = self.contentLengthOffset else { + guard let contentLengthOffset = self.contentLengthOffset, let file = self.file else { + self.isFailed = true + return + } + assert(contentLengthOffset >= 0) + + let metadataPosition = file.position() + file.seek(position: Int64(contentLengthOffset)) + file.write(UInt32(self.contentLength)) + + file.seek(position: metadataPosition) + file.write(UInt32(self.frames.count)) + for frame in self.frames { + file.write(UInt32(frame.offset)) + file.write(UInt32(frame.length)) + file.write(Float32(frame.duration)) + } + + if !self.frames.isEmpty, let dctCoefficients = self.currentDctCoefficients, let dctData = self.currentDctData { + var firstFrameData = Data(capacity: 4 * 5 + self.frames[0].length) + + writeUInt32(data: &firstFrameData, value: 2 as UInt32) + writeUInt32(data: &firstFrameData, value: UInt32(dctCoefficients.yPlane.width)) + writeUInt32(data: &firstFrameData, value: UInt32(dctCoefficients.yPlane.height)) + writeUInt32(data: &firstFrameData, value: UInt32(dctData.quality)) + + writeUInt32(data: &firstFrameData, value: UInt32(self.frames[0].length)) + let firstFrameStart = 4 * 5 + + file.seek(position: Int64(self.frames[0].offset)) + firstFrameData.count += self.frames[0].length + firstFrameData.withUnsafeMutableBytes { bytes in + let _ = file.read(bytes.baseAddress!.advanced(by: 4 * 5), self.frames[0].length) + } + + writeUInt32(data: &firstFrameData, value: UInt32(1)) + writeUInt32(data: &firstFrameData, value: UInt32(firstFrameStart)) + writeUInt32(data: &firstFrameData, value: UInt32(self.frames[0].length)) + writeFloat32(data: &firstFrameData, value: Float32(1.0)) + + guard let compressedFirstFrameData = compressData(data: firstFrameData, addSizeHeader: true) else { + self.isFailed = true + return + } + guard let _ = try? compressedFirstFrameData.write(to: URL(fileURLWithPath: self.firstFramePath)) else { + self.isFailed = true + return + } + } else { self.isFailed = true return } - let metadataPosition = self.file.position() - self.file.seek(position: Int64(contentLengthOffset)) - self.file.write(UInt32(self.contentLength)) - - self.file.seek(position: metadataPosition) - self.file.write(UInt32(self.frames.count)) - for frame in self.frames { - self.file.write(UInt32(frame.offset)) - self.file.write(UInt32(frame.length)) - self.file.write(Float32(frame.duration)) + if !self.isFailed { + self.file = nil + + file._unsafeClose() + + guard let uncompressedData = try? Data(contentsOf: URL(fileURLWithPath: self.decompressedPath), options: .alwaysMapped) else { + self.isFailed = true + return + } + guard let compressedData = compressData(data: uncompressedData) else { + self.isFailed = true + return + } + guard let compressedFile = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { + self.isFailed = true + return + } + compressedFile.write(Int32(uncompressedData.count)) + let _ = compressedFile.write(compressedData) + compressedFile._unsafeClose() } } } if shouldComplete { - self.completion(!self.isFailed) + let _ = try? FileManager.default.removeItem(atPath: self.decompressedPath) + + if !self.isFailed { + self.completion(CompressedResult( + animationPath: self.compressedPath, + firstFramePath: self.firstFramePath + )) + } else { + let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) + let _ = try? FileManager.default.removeItem(atPath: self.firstFramePath) + self.completion(nil) + } } } } @@ -185,12 +465,34 @@ private final class AnimationCacheItemAccessor { private let data: Data private let frameMapping: [Int: FrameInfo] - private let format: AnimationCacheItemFrame.Format + private let durationMapping: [Double] + private let totalDuration: Double - init(data: Data, frameMapping: [Int: FrameInfo], format: AnimationCacheItemFrame.Format) { + private var currentYUVASurface: ImageYUVA420 + private var currentDctData: DctData + private var currentDctCoefficients: DctCoefficientsYUVA420 + + init(data: Data, frameMapping: [FrameInfo], width: Int, height: Int, dctQuality: Int) { self.data = data - self.frameMapping = frameMapping - self.format = format + + var resultFrameMapping: [Int: FrameInfo] = [:] + var durationMapping: [Double] = [] + var totalDuration: Double = 0.0 + + for i in 0 ..< frameMapping.count { + let frame = frameMapping[i] + resultFrameMapping[i] = frame + totalDuration += frame.duration + durationMapping.append(totalDuration) + } + + self.frameMapping = resultFrameMapping + self.durationMapping = durationMapping + self.totalDuration = totalDuration + + self.currentYUVASurface = ImageYUVA420(width: width, height: height) + self.currentDctData = DctData(quality: dctQuality) + self.currentDctCoefficients = DctCoefficientsYUVA420(width: width, height: height) } func getFrame(index: Int) -> AnimationCacheItemFrame? { @@ -198,7 +500,56 @@ private final class AnimationCacheItemAccessor { return nil } - return AnimationCacheItemFrame(data: data, range: frameInfo.range, format: self.format, duration: frameInfo.duration) + let currentSurface = ImageARGB(width: self.currentYUVASurface.yPlane.width, height: self.currentYUVASurface.yPlane.height) + + var frameDataOffset = 0 + let frameLength = frameInfo.range.upperBound - frameInfo.range.lowerBound + for i in 0 ..< 4 { + let dctPlane: DctCoefficientPlane + switch i { + case 0: + dctPlane = self.currentDctCoefficients.yPlane + case 1: + dctPlane = self.currentDctCoefficients.uPlane + case 2: + dctPlane = self.currentDctCoefficients.vPlane + case 3: + dctPlane = self.currentDctCoefficients.aPlane + default: + preconditionFailure() + } + + if frameDataOffset + dctPlane.data.count > frameLength { + break + } + + dctPlane.data.withUnsafeMutableBytes { targetBuffer -> Void in + self.data.copyBytes(to: targetBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (frameInfo.range.lowerBound + frameDataOffset) ..< (frameInfo.range.lowerBound + frameDataOffset + targetBuffer.count)) + } + + frameDataOffset += dctPlane.data.count + } + + self.currentDctCoefficients.idct(dctData: self.currentDctData, target: self.currentYUVASurface) + self.currentYUVASurface.toARGB(target: currentSurface) + + return AnimationCacheItemFrame(data: currentSurface.argbPlane.data, range: 0 ..< currentSurface.argbPlane.data.count, format: .rgba(width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: frameInfo.duration) + } + + func getFrameIndex(duration: Double) -> Int { + if self.totalDuration == 0.0 { + return 0 + } + if self.durationMapping.count <= 1 { + return 0 + } + let normalizedDuration = duration.truncatingRemainder(dividingBy: self.totalDuration) + for i in 1 ..< self.durationMapping.count { + if normalizedDuration < self.durationMapping[i] { + return i - 1 + } + } + return self.durationMapping.count - 1 } } @@ -213,10 +564,54 @@ private func readUInt32(data: Data, offset: Int) -> UInt32 { return value } +private func readFloat32(data: Data, offset: Int) -> Float32 { + var value: Float32 = 0 + withUnsafeMutableBytes(of: &value, { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) + } + }) + + return value +} + +private func writeUInt32(data: inout Data, value: UInt32) { + var value: UInt32 = value + withUnsafeBytes(of: &value, { bytes -> Void in + data.count += 4 + data.withUnsafeMutableBytes { dataBytes -> Void in + memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) + } + }) +} + +private func writeFloat32(data: inout Data, value: Float32) { + var value: Float32 = value + withUnsafeBytes(of: &value, { bytes -> Void in + data.count += 4 + data.withUnsafeMutableBytes { dataBytes -> Void in + memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) + } + }) +} + private func loadItem(path: String) -> AnimationCacheItem? { - guard let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { + guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { return nil } + + if compressedData.count < 4 { + return nil + } + let decompressedSize = readUInt32(data: compressedData, offset: 0) + + if decompressedSize <= 0 || decompressedSize > 20 * 1024 * 1024 { + return nil + } + guard let data = decompressData(data: compressedData, range: 4 ..< compressedData.count, decompressedSize: Int(decompressedSize)) else { + return nil + } + let dataLength = data.count var offset = 0 @@ -226,7 +621,7 @@ private func loadItem(path: String) -> AnimationCacheItem? { } let formatVersion = readUInt32(data: data, offset: offset) offset += 4 - if formatVersion != 1 { + if formatVersion != 2 { return nil } @@ -245,7 +640,7 @@ private func loadItem(path: String) -> AnimationCacheItem? { guard dataLength >= offset + 4 else { return nil } - let bytesPerRow = readUInt32(data: data, offset: offset) + let dctQuality = readUInt32(data: data, offset: offset) offset += 4 guard dataLength >= offset + 4 else { @@ -262,8 +657,8 @@ private func loadItem(path: String) -> AnimationCacheItem? { let numFrames = readUInt32(data: data, offset: offset) offset += 4 - var frameMapping: [Int: AnimationCacheItemAccessor.FrameInfo] = [:] - for i in 0 ..< Int(numFrames) { + var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = [] + for _ in 0 ..< Int(numFrames) { guard dataLength >= offset + 4 + 4 + 4 else { return nil } @@ -272,16 +667,18 @@ private func loadItem(path: String) -> AnimationCacheItem? { offset += 4 let frameLength = readUInt32(data: data, offset: offset) offset += 4 - let frameDuration = readUInt32(data: data, offset: offset) + let frameDuration = readFloat32(data: data, offset: offset) offset += 4 - frameMapping[i] = AnimationCacheItemAccessor.FrameInfo(range: Int(frameStart) ..< Int(frameStart + frameLength), duration: Double(frameDuration)) + frameMapping.append(AnimationCacheItemAccessor.FrameInfo(range: Int(frameStart) ..< Int(frameStart + frameLength), duration: Double(frameDuration))) } - let itemAccessor = AnimationCacheItemAccessor(data: data, frameMapping: frameMapping, format: .rgba(width: Int(width), height: Int(height), bytesPerRow: Int(bytesPerRow))) + let itemAccessor = AnimationCacheItemAccessor(data: data, frameMapping: frameMapping, width: Int(width), height: Int(height), dctQuality: Int(dctQuality)) return AnimationCacheItem(numFrames: Int(numFrames), getFrame: { index in return itemAccessor.getFrame(index: index) + }, getFrameIndexImpl: { duration in + return itemAccessor.getFrameIndex(duration: duration) }) } @@ -300,10 +697,14 @@ public final class AnimationCacheImpl: AnimationCache { private let basePath: String private let allocateTempFile: () -> String + private let fetchQueues: [Queue] + private var nextFetchQueueIndex: Int = 0 + private var itemContexts: [String: ItemContext] = [:] init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String) { self.queue = queue + self.fetchQueues = (0 ..< 2).map { _ in Queue() } self.basePath = basePath self.allocateTempFile = allocateTempFile } @@ -315,9 +716,10 @@ public final class AnimationCacheImpl: AnimationCache { let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - if FileManager.default.fileExists(atPath: itemPath) { - updateResult(AnimationCacheItemResult(item: loadItem(path: itemPath), isFinal: true)) + if FileManager.default.fileExists(atPath: itemPath), let item = loadItem(path: itemPath) { + updateResult(AnimationCacheItemResult(item: item, isFinal: true)) return EmptyDisposable } @@ -338,8 +740,7 @@ public final class AnimationCacheImpl: AnimationCache { updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) if beginFetch { - let tempPath = self.allocateTempFile() - guard let writer = AnimationCacheItemWriterImpl(tempPath: tempPath, completion: { [weak self, weak itemContext] success in + guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[self.nextFetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in queue.async { guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[sourceId] else { return @@ -347,13 +748,18 @@ public final class AnimationCacheImpl: AnimationCache { strongSelf.itemContexts.removeValue(forKey: sourceId) - guard success else { + guard let result = result else { return } guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { return } - guard let _ = try? FileManager.default.moveItem(atPath: tempPath, toPath: itemPath) else { + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return + } + let _ = try? FileManager.default.removeItem(atPath: itemFirstFramePath) + guard let _ = try? FileManager.default.moveItem(atPath: result.firstFramePath, toPath: itemFirstFramePath) else { return } guard let item = loadItem(path: itemPath) else { @@ -368,9 +774,14 @@ public final class AnimationCacheImpl: AnimationCache { return EmptyDisposable } - let fetchDisposable = fetch(size, writer) + let fetchDisposable = MetaDisposable() + fetchDisposable.set(fetch(size, writer)) - itemContext.disposable.set(ActionDisposable { + itemContext.disposable.set(ActionDisposable { [weak writer] in + if let writer = writer { + writer.isCancelled = true + } + fetchDisposable.dispose() }) } @@ -389,25 +800,43 @@ public final class AnimationCacheImpl: AnimationCache { } } - func getSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { + static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize) -> AnimationCacheItem? { let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) - let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" - let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - if FileManager.default.fileExists(atPath: itemPath) { - return loadItem(path: itemPath) + if FileManager.default.fileExists(atPath: itemFirstFramePath) { + return loadItem(path: itemFirstFramePath) } else { return nil } } + + static func getFirstFrame(basePath: String, sourceId: String, size: CGSize, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = loadItem(path: itemFirstFramePath) { + completion(item) + + return EmptyDisposable + } else { + completion(nil) + + return EmptyDisposable + } + } } private let queue: Queue + private let basePath: String private let impl: QueueLocalObject 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 } } diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift new file mode 100644 index 0000000000..a30d8fd48f --- /dev/null +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift @@ -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 + } +} diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD index ff1faa52e0..37de259bd2 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD @@ -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", ], diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 051259f600..eecc39922f 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -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.infoDisposable = (context.engine.stickers.loadedStickerPack(reference: emoji.stickerPack, forceActualized: false) + |> deliverOnMainQueue).start(next: { [weak self] result 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 - 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 + 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 + } } - - 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 = 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)) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index f8ed135071..5c03454d8a 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -301,6 +301,39 @@ public final class EmojiPagerContentComponent: Component { super.init() if file.isAnimatedSticker || file.isVideoSticker { + let loadAnimation: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } + + 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 + 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() + } + }) + } + if attemptSynchronousLoad { if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { self.displayPlaceholder = true @@ -309,34 +342,13 @@ public final class EmojiPagerContentComponent: Component { 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 { - 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) - } + loadAnimation() + } else { + let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { _ in + loadAnimation() }) - - let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start() - - return ActionDisposable { - dataDisposable.dispose() - fetchDisposable.dispose() - } - }) + } } 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 } diff --git a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift index 1049ce1d21..a3f4e96a63 100644 --- a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift +++ b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift @@ -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 - guard let animation = LottieInstance(data: decompressedData, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else { + 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 frameDuration = 1.0 / Double(animation.frameRate) + for i in 0 ..< animation.frameCount { + 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() - 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) - } - - writer.finish() } diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index d399ea98cc..c952603feb 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -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 - 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>() - 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) { - target.updateDisplayPlaceholder(displayPlaceholder: false) - target.contents = currentFrameGroup.image.cgImage - target.contentsRect = contentsRect - } + if let currentFrameGroup = self.currentFrameGroup { + target.updateDisplayPlaceholder(displayPlaceholder: false) + target.contents = currentFrameGroup.image.cgImage } 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)) } } diff --git a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift index c052c0b77b..ddd56772f7 100644 --- a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift +++ b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift @@ -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 writer.isCancelled { + break + } 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) - } + + 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 } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e891ae810d..5ee132edaf 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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 diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 8c9f9cd9fc..662db807d6 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -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,37 +105,33 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { }) let animationRenderer = MultiAnimationRendererImpl() - let emojiItems: Signal = 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 = 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() + 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, - file: item.file - )) - } + 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] = [] @@ -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 = combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000), hasPremium diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 4bd4d9df65..b66b0bb07a 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -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 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(emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: stickerPack, fileId: fileId)) - 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) - - let insertString = NSAttributedString(string: "[\u{00a0}\u{00a0}]", attributes: updatedAttributes) - updatedString.replaceCharacters(in: range, with: insertString) - } + 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 diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index c4798cd366..59a51c5c83 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -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() + 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 diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index f9dbd1d6e7..3bdae8b476 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -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, emojiViewProvider: ((String) -> UIView)?) -> NSAttributedString { +public func textAttributedStringForStateText(_ stateText: NSAttributedString, fontSize: CGFloat, textColor: UIColor, accentTextColor: UIColor, writingDirection: NSWritingDirection?, spoilersRevealed: Bool, availableEmojis: Set, 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, emojiViewProvider: ((String) -> UIView)?) { +public func refreshChatTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, spoilersRevealed: Bool, availableEmojis: Set, 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, emojiViewProvider: ((String) -> UIView)?, spoilersRevealed: Bool = false) { +public func refreshGenericTextInputAttributes(_ textNode: ASEditableTextNode, theme: PresentationTheme, baseFontSize: CGFloat, availableEmojis: Set, 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 diff --git a/submodules/TextFormat/Sources/GenerateTextEntities.swift b/submodules/TextFormat/Sources/GenerateTextEntities.swift index dbaa90b7a6..35efbe4f81 100644 --- a/submodules/TextFormat/Sources/GenerateTextEntities.swift +++ b/submodules/TextFormat/Sources/GenerateTextEntities.swift @@ -145,26 +145,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 { @@ -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))) } } }) diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index afde2592a1..2d6ebff158 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -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 }