TM-SGNL-iOS/SignalServiceKit/Network/API/Giphy/GiphyImageInfo.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

152 lines
5 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public enum GiphyError: Error {
case assertionError(description: String)
case fetchFailure
}
extension GiphyError: LocalizedError, UserErrorDescriptionProvider {
public var errorDescription: String? {
localizedDescription
}
public var localizedDescription: String {
switch self {
case .assertionError:
return OWSLocalizedString("GIF_PICKER_ERROR_GENERIC", comment: "Generic error displayed when picking a GIF")
case .fetchFailure:
return OWSLocalizedString("GIF_PICKER_ERROR_FETCH_FAILURE", comment: "Error displayed when there is a failure fetching a GIF from the remote service.")
}
}
}
// Represents a single Giphy image.
@objc
public class GiphyImageInfo: NSObject {
public let giphyId: String
private let assets: [GiphyAsset]
init?(parsing dictionary: [String: Any]) {
guard let idString = dictionary["id"] as? String,
let renditionDict = (dictionary["images"] as? [String: [String: Any]]) else {
Logger.warn("Missing required parameters")
return nil
}
giphyId = idString
assets = renditionDict.flatMap { (rendition, dict) in
GiphyAsset.parsing(renditionString: rendition, definition: dict)
}
super.init()
if giphyId.isEmpty {
Logger.error("Invalid id when parsing image info")
return nil
}
guard isValidImage else {
Logger.error("Missing required asset info")
return nil
}
}
// TODO: We may need to tweak these constants.
let kValidPreviewDimensions: ClosedRange<CGFloat> = 60...618
let kValidSendingDimensions: ClosedRange<CGFloat> = 101...618
let kPreferedPreviewFileSize = Int(256 * 1024)
let kPreferedSendingFileSize = Int(3 * 1024 * 1024)
}
extension GiphyImageInfo {
public var isValidImage: Bool {
[anyOriginalAsset, animatedPreviewAsset, fullSizeAsset]
.allSatisfy { $0 != nil }
}
public var animatedPreviewAsset: GiphyAsset? {
assets
.filter { !$0.rendition.isStill }
.filter { [.gif, .mp4].contains($0.type) }
.filter { $0.dimensions.fits(range: kValidPreviewDimensions) }
.filter { $0.size > 0 }
.bestOption(forTargetSize: kPreferedPreviewFileSize)
}
public var fullSizeAsset: GiphyAsset? {
let validTypes: [GiphyAsset.FileType] = [.gif, .mp4]
return assets
.filter { !$0.rendition.isStill }
.filter { validTypes.contains($0.type) }
.filter { $0.dimensions.fits(range: kValidSendingDimensions) }
.filter { $0.size > 0 }
.bestOption(forTargetSize: kPreferedSendingFileSize)
}
public var originalAspectRatio: CGFloat {
// Only the original rendition has the aspect ratio source of truth
anyOriginalAsset.map { $0.dimensions.width / $0.dimensions.height } ?? 1.0
}
private var anyOriginalAsset: GiphyAsset? {
assets.first { $0.rendition == .original }
}
}
private extension Sequence where Element == GiphyAsset {
// Selects the largest element under the target size, or if not satisfiable,
// the smallest element above the target size
// Given a sequence of assets, returns...
// - The largest dimensioned item under the target file size
// - If unavailable, the item with the smallest file size over the target
func bestOption(forTargetSize targetSize: Int) -> GiphyAsset? {
let findLargestUnderBudget = {
filter { $0.size <= targetSize }.max {
// Order by increasing width. If equal, order by decreasing file size.
if $0.dimensions.width != $1.dimensions.width {
return $0.dimensions.width < $1.dimensions.width
} else {
return $0.size > $1.size
}
}
}
let budgetWindow = (targetSize+1..<Int(OWSMediaUtils.kMaxFileSizeImage))
let findSmallestOverBudget = {
filter { budgetWindow.contains($0.size) }.min {
// Order by increasing file size. If equal, order by decreasing dimension.
if $0.size != $1.size {
return $0.size < $1.size
} else {
return $0.dimensions.width > $1.dimensions.width
}
}
}
return findLargestUnderBudget() ?? findSmallestOverBudget()
}
}
private extension CGSize {
func fits<T>(range: T) -> Bool where T: RangeExpression, T.Bound == CGFloat {
range.contains(width) && range.contains(height)
}
}
private extension URL {
var giphyAssetFileExtension: String? {
let urlExtension = pathExtension.lowercased()
if ["gif", "mp4", "jpg"].contains(urlExtension) {
return urlExtension
} else {
Logger.error("Invalid file extension from giphy")
return nil
}
}
}