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)
}
}
}
}