iOS/WAStickersThirdParty/StickerPack.swift (129 lines of code) (raw):

// // Copyright (c) WhatsApp Inc. and its affiliates. // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // import UIKit /** * Represents a variety of errors related to stickers. */ enum StickerPackError: Error { case fileNotFound case emptyString case unsupportedImageFormat(String) case imageTooBig(Int64, Bool) // Bool value indicates whether the image is animated case invalidImage case incorrectImageSize(CGSize) case animatedImagesNotSupported case stickersNumOutsideAllowableRange case stringTooLong case tooManyEmojis case minFrameDurationTooShort(Double) case totalAnimationDurationTooLong(Double) case animatedStickerPackWithStaticStickers case staticStickerPackWithAnimatedStickers } /** * Main class that handles sticker packs, a set of stickers. */ class StickerPack { let identifier: String let name: String let publisher: String let trayImage: ImageData let publisherWebsite: String? let privacyPolicyWebsite: String? let licenseAgreementWebsite: String? var animated: Bool var stickers: [Sticker] var bytesSize: Int64 { var totalBytes: Int64 = Int64(name.utf8.count + publisher.utf8.count + trayImage.data.count) stickers.forEach { totalBytes += $0.bytesSize } return totalBytes } var formattedSize: String { return ByteCountFormatter.string(fromByteCount: bytesSize, countStyle: .file) } /** * Initializes a sticker pack with a name, publisher and tray image name. * * - Parameter name: title of the sticker pack * - Parameter publisher: publisher of the sticker pack * - Parameter publisherWebsite: website of publisher * - Parameter privacyPolicyWebsite: website of privacy policy * - Parameter licenseAgreementWebsite: website of license agreement * * - Throws: - .emptyString if the name and publisher are empty strings - .stringTooLong if the name and publisher are more than 128 character - .fileNotFound if tray image file has not been found - .unsupportedImageFormat if tray image file is not png or webp - .imageTooBig if the tray image file size is above the supported limit (50KB) - .invalidImage if the image file size is 0KB - .incorrectImageSize if the tray image is not within the allowed size - .animatedImagesNotSupported if the tray image is animated */ init(identifier: String, name: String, publisher: String, trayImageFileName: String, animatedStickerPack: Bool?, publisherWebsite: String?, privacyPolicyWebsite: String?, licenseAgreementWebsite: String?) throws { guard !name.isEmpty && !publisher.isEmpty && !identifier.isEmpty else { throw StickerPackError.emptyString } guard name.count <= Limits.MaxCharLimit128 && publisher.count <= Limits.MaxCharLimit128 && identifier.count <= Limits.MaxCharLimit128 else { throw StickerPackError.stringTooLong } self.identifier = identifier self.name = name self.publisher = publisher let trayCompliantImageData: ImageData = try ImageData.imageDataIfCompliant(contentsOfFile: trayImageFileName, isTray: true) self.trayImage = trayCompliantImageData self.animated = animatedStickerPack ?? false stickers = [] self.publisherWebsite = publisherWebsite self.privacyPolicyWebsite = privacyPolicyWebsite self.licenseAgreementWebsite = licenseAgreementWebsite } /** * Initializes a sticker pack with a name, publisher and tray image data. * * - Paramter identifier: identifier of the sticker pack * - Parameter name: title of the sticker pack * - Parameter publisher: publisher of the sticker pack * - Parameter trayImagePNGData: the PNG data of the tray image * - Parameter publisherWebsite: website of publisher * - Parameter privacyPolicyWebsite: website of privacy policy * - Parameter licenseAgreementWebsite: website of license agreement * * - Throws: - .emptyString if any string parameter is empty - .stringTooLong if any string is too long - .imageTooBig if the tray image file size is above the supported limit (50KB) - .invalidImage if the image file size is 0KB - .incorrectImageSize if the tray image is not within the allowed size - .animatedImagesNotSupported if the tray image is animated */ init(identifier: String, name: String, publisher: String, trayImagePNGData: Data, publisherWebsite: String?, privacyPolicyWebsite: String?, licenseAgreementWebsite: String?) throws { guard !name.isEmpty && !publisher.isEmpty && !identifier.isEmpty else { throw StickerPackError.emptyString } guard name.count <= Limits.MaxCharLimit128 && publisher.count <= Limits.MaxCharLimit128 && identifier.count <= Limits.MaxCharLimit128 else { throw StickerPackError.stringTooLong } self.identifier = identifier self.name = name self.publisher = publisher let trayCompliantImageData: ImageData = try ImageData.imageDataIfCompliant(rawData: trayImagePNGData, extensionType: .png, isTray: true) self.trayImage = trayCompliantImageData self.animated = false stickers = [] self.publisherWebsite = publisherWebsite self.privacyPolicyWebsite = privacyPolicyWebsite self.licenseAgreementWebsite = licenseAgreementWebsite } /** * Adds a sticker to the current sticker pack. * * - Parameter filename: file name of the sticker (png or webp). * - Parameter emojis: emojis associated with the sticker. * * - Throws: - .stickersNumOutsideAllowableRange if current number of stickers is not withing limits - .animatedStickerPackWithStaticStickers if an animated pack contains static stickers - .staticStickerPackWithAnimatedStickers if a static pack contains animated stickers - All exceptions from Sticker(contentsOfFile:emojis:) */ func addSticker(contentsOfFile filename: String, emojis: [String]?) throws { guard stickers.count <= Limits.MaxStickersPerPack else { throw StickerPackError.stickersNumOutsideAllowableRange } let sticker: Sticker = try Sticker(contentsOfFile: filename, emojis: emojis) guard sticker.imageData.animated == self.animated else { if self.animated { throw StickerPackError.animatedStickerPackWithStaticStickers } else { throw StickerPackError.staticStickerPackWithAnimatedStickers } } stickers.append(sticker) } /** * Adds a sticker to the current sticker pack. * * - Parameter imageData: image data of the sticker * - Parameter type: extension type of the data (png or webp) * - Parameter emojis: emojis associated with the sticker. * * - Throws: - .stickersNumOutsideAllowableRange if current number of stickers is not withing limits - .animatedStickerPackWithStaticStickers if an animated pack contains static stickers - .staticStickerPackWithAnimatedStickers if a static pack contains animated stickers - All exceptions from Sticker(imageData:type:emojis:) */ func addSticker(imageData: Data, type: ImageDataExtension, emojis: [String]?) throws { guard stickers.count <= Limits.MaxStickersPerPack else { throw StickerPackError.stickersNumOutsideAllowableRange } let sticker: Sticker = try Sticker(imageData: imageData, type: type, emojis: emojis) guard sticker.imageData.animated == self.animated else { if self.animated { throw StickerPackError.animatedStickerPackWithStaticStickers } else { throw StickerPackError.staticStickerPackWithAnimatedStickers } } stickers.append(sticker) } /** * Sends current sticker pack to WhatsApp. * * - Parameter completionHandler: block that gets called when the sticker pack has been wrapped * into a format that WhatsApp can read and WhatsApp is about to open. Called on the main * queue. */ func sendToWhatsApp(completionHandler: @escaping (Bool) -> Void) { StickerPackManager.queue.async { var json: [String: Any] = [:] json["identifier"] = self.identifier json["name"] = self.name json["publisher"] = self.publisher json["tray_image"] = self.trayImage.image!.pngData()?.base64EncodedString() if self.animated { json["animated_sticker_pack"] = self.animated } var stickersArray: [[String: Any]] = [] for sticker in self.stickers { var stickerDict: [String: Any] = [:] if let imageData = sticker.imageData.webpData { stickerDict["image_data"] = imageData.base64EncodedString() } else { print("Skipping bad sticker data") continue } stickerDict["emojis"] = sticker.emojis stickersArray.append(stickerDict) } json["stickers"] = stickersArray let result = Interoperability.send(json: json) DispatchQueue.main.async { completionHandler(result) } } } }