|
|
|
@ -6,6 +6,7 @@ import Foundation
|
|
|
|
|
import MobileCoreServices
|
|
|
|
|
|
|
|
|
|
enum SignalAttachmentError: String {
|
|
|
|
|
case missingData
|
|
|
|
|
case fileSizeTooLarge
|
|
|
|
|
case invalidData
|
|
|
|
|
case couldNotParseImage
|
|
|
|
@ -42,12 +43,12 @@ class SignalAttachment: NSObject {
|
|
|
|
|
|
|
|
|
|
// MARK: Properties
|
|
|
|
|
|
|
|
|
|
let data: Data!
|
|
|
|
|
let data: Data
|
|
|
|
|
|
|
|
|
|
// Attachment types are identified using UTIs.
|
|
|
|
|
//
|
|
|
|
|
// See: https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html
|
|
|
|
|
let dataUTI: String!
|
|
|
|
|
let dataUTI: String
|
|
|
|
|
|
|
|
|
|
var error: SignalAttachmentError? {
|
|
|
|
|
didSet {
|
|
|
|
@ -67,32 +68,32 @@ class SignalAttachment: NSObject {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Media Size constraints from Signal-Android
|
|
|
|
|
* (org/thoughtcrime/securesms/mms/PushMediaConstraints.java)
|
|
|
|
|
*
|
|
|
|
|
* https://github.com/WhisperSystems/Signal-Android/blob/master/src/org/thoughtcrime/securesms/mms/PushMediaConstraints.java
|
|
|
|
|
*/
|
|
|
|
|
static let kMaxFileSizeGif = 5 * 1024 * 1024
|
|
|
|
|
static let kMaxFileSizeImage = 420 * 1024
|
|
|
|
|
static let kMaxFileSizeAnimatedImage = 6 * 1024 * 1024
|
|
|
|
|
static let kMaxFileSizeImage = 6 * 1024 * 1024
|
|
|
|
|
static let kMaxFileSizeVideo = 100 * 1024 * 1024
|
|
|
|
|
static let kMaxFileSizeAudio = 100 * 1024 * 1024
|
|
|
|
|
// TODO: What should the max file size on "other" attachments be?
|
|
|
|
|
static let kMaxFileSizeGeneric = 100 * 1024 * 1024
|
|
|
|
|
|
|
|
|
|
// MARK: Constructor
|
|
|
|
|
|
|
|
|
|
// This method should not be called directly; use the factory
|
|
|
|
|
// methods instead.
|
|
|
|
|
internal required init(data: Data!, dataUTI: String!) {
|
|
|
|
|
internal required init(data: Data, dataUTI: String) {
|
|
|
|
|
self.data = data
|
|
|
|
|
self.dataUTI = dataUTI
|
|
|
|
|
super.init()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func hasError() -> Bool {
|
|
|
|
|
var hasError: Bool {
|
|
|
|
|
return error != nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the MIME type for this attachment or nil if no MIME type
|
|
|
|
|
// can be identified.
|
|
|
|
|
public func mimeType() -> String? {
|
|
|
|
|
var mimeType: String? {
|
|
|
|
|
let mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)
|
|
|
|
|
guard mimeType != nil else {
|
|
|
|
|
return nil
|
|
|
|
@ -102,12 +103,12 @@ class SignalAttachment: NSObject {
|
|
|
|
|
|
|
|
|
|
// Returns the file extension for this attachment or nil if no file extension
|
|
|
|
|
// can be identified.
|
|
|
|
|
public func fileExtension() -> String? {
|
|
|
|
|
let fileExtension = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassFilenameExtension)
|
|
|
|
|
guard fileExtension != nil else {
|
|
|
|
|
var fileExtension: String? {
|
|
|
|
|
guard let fileExtension = UTTypeCopyPreferredTagWithClass(dataUTI as CFString,
|
|
|
|
|
kUTTagClassFilenameExtension) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return fileExtension?.takeRetainedValue() as? String
|
|
|
|
|
return fileExtension.takeRetainedValue() as String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the set of UTIs that correspond to valid _input_ image formats
|
|
|
|
@ -115,65 +116,50 @@ class SignalAttachment: NSObject {
|
|
|
|
|
//
|
|
|
|
|
// Image attachments may be converted to another image format before
|
|
|
|
|
// being uploaded.
|
|
|
|
|
//
|
|
|
|
|
// TODO: We need to finalize which formats we support.
|
|
|
|
|
private class func inputImageUTISet() -> Set<String>! {
|
|
|
|
|
return [
|
|
|
|
|
kUTTypeJPEG as String,
|
|
|
|
|
kUTTypeGIF as String,
|
|
|
|
|
kUTTypePNG as String
|
|
|
|
|
]
|
|
|
|
|
private class var inputImageUTISet: Set<String> {
|
|
|
|
|
return MIMETypeUtil.supportedImageUTITypes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the set of UTIs that correspond to valid _output_ image formats
|
|
|
|
|
// for Signal attachments.
|
|
|
|
|
//
|
|
|
|
|
// TODO: We need to finalize which formats we support.
|
|
|
|
|
private class func outputImageUTISet() -> Set<String>! {
|
|
|
|
|
return [
|
|
|
|
|
kUTTypeJPEG as String,
|
|
|
|
|
kUTTypeGIF as String,
|
|
|
|
|
kUTTypePNG as String
|
|
|
|
|
]
|
|
|
|
|
private class var outputImageUTISet: Set<String> {
|
|
|
|
|
return MIMETypeUtil.supportedImageUTITypes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the set of UTIs that correspond to valid animated image formats
|
|
|
|
|
// for Signal attachments.
|
|
|
|
|
private class var animatedImageUTISet: Set<String> {
|
|
|
|
|
return MIMETypeUtil.supportedAnimatedImageUTITypes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the set of UTIs that correspond to valid video formats
|
|
|
|
|
// for Signal attachments.
|
|
|
|
|
//
|
|
|
|
|
// TODO: We need to finalize which formats we support.
|
|
|
|
|
private class func videoUTISet() -> Set<String>! {
|
|
|
|
|
return [
|
|
|
|
|
kUTTypeMPEG4 as String
|
|
|
|
|
]
|
|
|
|
|
private class var videoUTISet: Set<String> {
|
|
|
|
|
return MIMETypeUtil.supportedVideoUTITypes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the set of UTIs that correspond to valid audio formats
|
|
|
|
|
// for Signal attachments.
|
|
|
|
|
//
|
|
|
|
|
// TODO: We need to finalize which formats we support.
|
|
|
|
|
private class func audioUTISet() -> Set<String>! {
|
|
|
|
|
return [
|
|
|
|
|
kUTTypeMP3 as String,
|
|
|
|
|
kUTTypeMPEG4Audio as String
|
|
|
|
|
]
|
|
|
|
|
private class var audioUTISet: Set<String> {
|
|
|
|
|
return MIMETypeUtil.supportedAudioUTITypes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the set of UTIs that correspond to valid input formats
|
|
|
|
|
// for Signal attachments.
|
|
|
|
|
public class func validInputUTISet() -> Set<String>! {
|
|
|
|
|
return inputImageUTISet().union(videoUTISet().union(audioUTISet()))
|
|
|
|
|
public class var validInputUTISet: Set<String> {
|
|
|
|
|
return inputImageUTISet.union(videoUTISet.union(audioUTISet))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func isImage() -> Bool {
|
|
|
|
|
return SignalAttachment.outputImageUTISet().contains(dataUTI)
|
|
|
|
|
public var isImage: Bool {
|
|
|
|
|
return SignalAttachment.outputImageUTISet.contains(dataUTI)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func isVideo() -> Bool {
|
|
|
|
|
return SignalAttachment.videoUTISet().contains(dataUTI)
|
|
|
|
|
public var isVideo: Bool {
|
|
|
|
|
return SignalAttachment.videoUTISet.contains(dataUTI)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func isAudio() -> Bool {
|
|
|
|
|
return SignalAttachment.audioUTISet().contains(dataUTI)
|
|
|
|
|
public var isAudio: Bool {
|
|
|
|
|
return SignalAttachment.audioUTISet.contains(dataUTI)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns an attachment from the pasteboard, or nil if no attachment
|
|
|
|
@ -182,29 +168,31 @@ class SignalAttachment: NSObject {
|
|
|
|
|
// NOTE: The attachment returned by this method may not be valid.
|
|
|
|
|
// Check the attachment's error property.
|
|
|
|
|
public class func attachmentFromPasteboard() -> SignalAttachment? {
|
|
|
|
|
guard UIPasteboard.general.numberOfItems == 1 else {
|
|
|
|
|
// Ignore pasteboard if it contains multiple items.
|
|
|
|
|
//
|
|
|
|
|
// TODO: Should we try to use the first?
|
|
|
|
|
guard UIPasteboard.general.numberOfItems >= 1 else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
// If pasteboard contains multiple items, use only the first.
|
|
|
|
|
let itemSet = IndexSet(integer:0)
|
|
|
|
|
guard let pasteboardUTITypes = UIPasteboard.general.types(forItemSet:itemSet) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
let pasteboardUTISet = Set(UIPasteboard.general.types)
|
|
|
|
|
for dataUTI in inputImageUTISet() {
|
|
|
|
|
let pasteboardUTISet = Set<String>(pasteboardUTITypes[0])
|
|
|
|
|
for dataUTI in inputImageUTISet {
|
|
|
|
|
if pasteboardUTISet.contains(dataUTI) {
|
|
|
|
|
let imageData = UIPasteboard.general.data(forPasteboardType:dataUTI)
|
|
|
|
|
return imageAttachment(withData : imageData, dataUTI : dataUTI)
|
|
|
|
|
let data = UIPasteboard.general.data(forPasteboardType:dataUTI, inItemSet:itemSet)![0] as! Data
|
|
|
|
|
return imageAttachment(data : data, dataUTI : dataUTI)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for dataUTI in videoUTISet() {
|
|
|
|
|
for dataUTI in videoUTISet {
|
|
|
|
|
if pasteboardUTISet.contains(dataUTI) {
|
|
|
|
|
let imageData = UIPasteboard.general.data(forPasteboardType:dataUTI)
|
|
|
|
|
return videoAttachment(withData : imageData, dataUTI : dataUTI)
|
|
|
|
|
let data = UIPasteboard.general.data(forPasteboardType:dataUTI, inItemSet:itemSet)![0] as! Data
|
|
|
|
|
return videoAttachment(data : data, dataUTI : dataUTI)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for dataUTI in audioUTISet() {
|
|
|
|
|
for dataUTI in audioUTISet {
|
|
|
|
|
if pasteboardUTISet.contains(dataUTI) {
|
|
|
|
|
let imageData = UIPasteboard.general.data(forPasteboardType:dataUTI)
|
|
|
|
|
return audioAttachment(withData : imageData, dataUTI : dataUTI)
|
|
|
|
|
let data = UIPasteboard.general.data(forPasteboardType:dataUTI, inItemSet:itemSet)![0] as! Data
|
|
|
|
|
return audioAttachment(data : data, dataUTI : dataUTI)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// TODO: We could handle generic attachments at this point.
|
|
|
|
@ -218,17 +206,19 @@ class SignalAttachment: NSObject {
|
|
|
|
|
//
|
|
|
|
|
// NOTE: The attachment returned by this method may not be valid.
|
|
|
|
|
// Check the attachment's error property.
|
|
|
|
|
public class func imageAttachment(withData imageData: Data?, dataUTI: String!) -> SignalAttachment! {
|
|
|
|
|
public class func imageAttachment(data imageData: Data?, dataUTI: String) -> SignalAttachment {
|
|
|
|
|
assert(dataUTI.characters.count > 0)
|
|
|
|
|
|
|
|
|
|
assert(imageData != nil)
|
|
|
|
|
guard let imageData = imageData else {
|
|
|
|
|
return nil
|
|
|
|
|
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
|
|
|
|
|
attachment.error = .missingData
|
|
|
|
|
return attachment
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let attachment = SignalAttachment(data : imageData, dataUTI: dataUTI)
|
|
|
|
|
|
|
|
|
|
guard inputImageUTISet().contains(dataUTI) else {
|
|
|
|
|
guard inputImageUTISet.contains(dataUTI) else {
|
|
|
|
|
attachment.error = .invalidFileFormat
|
|
|
|
|
return attachment
|
|
|
|
|
}
|
|
|
|
@ -239,16 +229,13 @@ class SignalAttachment: NSObject {
|
|
|
|
|
return attachment
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if dataUTI == kUTTypeGIF as String {
|
|
|
|
|
guard imageData.count <= kMaxFileSizeGif else {
|
|
|
|
|
if animatedImageUTISet.contains(dataUTI) {
|
|
|
|
|
guard imageData.count <= kMaxFileSizeAnimatedImage else {
|
|
|
|
|
attachment.error = .fileSizeTooLarge
|
|
|
|
|
return attachment
|
|
|
|
|
}
|
|
|
|
|
// We don't re-encode GIFs as JPEGs, presumably in case they are
|
|
|
|
|
// animated.
|
|
|
|
|
//
|
|
|
|
|
// TODO: Consider re-encoding non-animated GIFs as JPEG?
|
|
|
|
|
Logger.verbose("\(TAG) Sending raw \(attachment.mimeType()) to retain any animation")
|
|
|
|
|
// Never re-encode animated images (i.e. GIFs) as JPEGs.
|
|
|
|
|
Logger.verbose("\(TAG) Sending raw \(attachment.mimeType) to retain any animation")
|
|
|
|
|
return attachment
|
|
|
|
|
} else {
|
|
|
|
|
guard let image = UIImage(data:imageData) else {
|
|
|
|
@ -258,7 +245,7 @@ class SignalAttachment: NSObject {
|
|
|
|
|
attachment.image = image
|
|
|
|
|
|
|
|
|
|
if isInputImageValidOutputImage(image: image, imageData: imageData, dataUTI: dataUTI) {
|
|
|
|
|
Logger.verbose("\(TAG) Sending raw \(attachment.mimeType())")
|
|
|
|
|
Logger.verbose("\(TAG) Sending raw \(attachment.mimeType)")
|
|
|
|
|
return attachment
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -268,31 +255,26 @@ class SignalAttachment: NSObject {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class func defaultImageUploadQuality() -> TSImageQuality {
|
|
|
|
|
// always return average image quality
|
|
|
|
|
//
|
|
|
|
|
// TODO: Why are we using this value? Why not default to .uncropped?
|
|
|
|
|
return .medium
|
|
|
|
|
// Always default to the highest possible image quality and
|
|
|
|
|
// not converting the image if possible.
|
|
|
|
|
return .uncropped
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the proposed attachment already is a JPEG, and already conforms to the
|
|
|
|
|
// If the proposed attachment already conforms to the
|
|
|
|
|
// file size and content size limits, don't recompress it.
|
|
|
|
|
//
|
|
|
|
|
// TODO: Should non-JPEGs always be converted to JPEG?
|
|
|
|
|
private class func isInputImageValidOutputImage(image: UIImage?, imageData: Data?, dataUTI: String!) -> Bool {
|
|
|
|
|
private class func isInputImageValidOutputImage(image: UIImage?, imageData: Data?, dataUTI: String) -> Bool {
|
|
|
|
|
guard let image = image else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
guard let imageData = imageData else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if dataUTI == kUTTypeJPEG as String {
|
|
|
|
|
let maxSize = maxSizeForImage(image: image,
|
|
|
|
|
imageUploadQuality:defaultImageUploadQuality())
|
|
|
|
|
if image.size.width <= maxSize &&
|
|
|
|
|
image.size.height <= maxSize &&
|
|
|
|
|
imageData.count <= kMaxFileSizeImage {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
let maxSize = maxSizeForImage(image: image,
|
|
|
|
|
imageUploadQuality:defaultImageUploadQuality())
|
|
|
|
|
if image.size.width <= maxSize &&
|
|
|
|
|
image.size.height <= maxSize &&
|
|
|
|
|
imageData.count <= kMaxFileSizeImage {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
@ -301,22 +283,24 @@ class SignalAttachment: NSObject {
|
|
|
|
|
//
|
|
|
|
|
// NOTE: The attachment returned by this method may nil or not be valid.
|
|
|
|
|
// Check the attachment's error property.
|
|
|
|
|
public class func imageAttachment(withImage image: UIImage?, dataUTI: String!) -> SignalAttachment! {
|
|
|
|
|
public class func imageAttachment(image: UIImage?, dataUTI: String) -> SignalAttachment {
|
|
|
|
|
assert(dataUTI.characters.count > 0)
|
|
|
|
|
|
|
|
|
|
guard let image = image else {
|
|
|
|
|
return nil
|
|
|
|
|
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
|
|
|
|
|
attachment.error = .missingData
|
|
|
|
|
return attachment
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make a placeholder attachment on which to hang errors if necessary.
|
|
|
|
|
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
|
|
|
|
|
attachment.image = image
|
|
|
|
|
|
|
|
|
|
Logger.verbose("\(TAG) Writing \(attachment.mimeType()) as image/jpeg")
|
|
|
|
|
Logger.verbose("\(TAG) Writing \(attachment.mimeType) as image/jpeg")
|
|
|
|
|
return compressImageAsJPEG(image : image, attachment : attachment)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class func compressImageAsJPEG(image: UIImage!, attachment: SignalAttachment!) -> SignalAttachment! {
|
|
|
|
|
private class func compressImageAsJPEG(image: UIImage, attachment: SignalAttachment) -> SignalAttachment {
|
|
|
|
|
assert(attachment.error == nil)
|
|
|
|
|
|
|
|
|
|
var imageUploadQuality = defaultImageUploadQuality()
|
|
|
|
@ -405,10 +389,10 @@ class SignalAttachment: NSObject {
|
|
|
|
|
//
|
|
|
|
|
// NOTE: The attachment returned by this method may not be valid.
|
|
|
|
|
// Check the attachment's error property.
|
|
|
|
|
public class func videoAttachment(withData data: Data?, dataUTI: String!) -> SignalAttachment! {
|
|
|
|
|
return newAttachment(withData : data,
|
|
|
|
|
public class func videoAttachment(data: Data?, dataUTI: String) -> SignalAttachment {
|
|
|
|
|
return newAttachment(data : data,
|
|
|
|
|
dataUTI : dataUTI,
|
|
|
|
|
validUTISet : videoUTISet(),
|
|
|
|
|
validUTISet : videoUTISet,
|
|
|
|
|
maxFileSize : kMaxFileSizeVideo)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -418,10 +402,10 @@ class SignalAttachment: NSObject {
|
|
|
|
|
//
|
|
|
|
|
// NOTE: The attachment returned by this method may not be valid.
|
|
|
|
|
// Check the attachment's error property.
|
|
|
|
|
public class func audioAttachment(withData data: Data?, dataUTI: String!) -> SignalAttachment! {
|
|
|
|
|
return newAttachment(withData : data,
|
|
|
|
|
public class func audioAttachment(data: Data?, dataUTI: String) -> SignalAttachment {
|
|
|
|
|
return newAttachment(data : data,
|
|
|
|
|
dataUTI : dataUTI,
|
|
|
|
|
validUTISet : audioUTISet(),
|
|
|
|
|
validUTISet : audioUTISet,
|
|
|
|
|
maxFileSize : kMaxFileSizeAudio)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -431,8 +415,8 @@ class SignalAttachment: NSObject {
|
|
|
|
|
//
|
|
|
|
|
// NOTE: The attachment returned by this method may not be valid.
|
|
|
|
|
// Check the attachment's error property.
|
|
|
|
|
public class func genericAttachment(withData data: Data?, dataUTI: String!) -> SignalAttachment! {
|
|
|
|
|
return newAttachment(withData : data,
|
|
|
|
|
public class func genericAttachment(data: Data?, dataUTI: String) -> SignalAttachment {
|
|
|
|
|
return newAttachment(data : data,
|
|
|
|
|
dataUTI : dataUTI,
|
|
|
|
|
validUTISet : nil,
|
|
|
|
|
maxFileSize : kMaxFileSizeGeneric)
|
|
|
|
@ -440,21 +424,23 @@ class SignalAttachment: NSObject {
|
|
|
|
|
|
|
|
|
|
// MARK: Helper Methods
|
|
|
|
|
|
|
|
|
|
private class func newAttachment(withData data: Data?,
|
|
|
|
|
dataUTI: String!,
|
|
|
|
|
private class func newAttachment(data: Data?,
|
|
|
|
|
dataUTI: String,
|
|
|
|
|
validUTISet: Set<String>?,
|
|
|
|
|
maxFileSize: Int) -> SignalAttachment! {
|
|
|
|
|
maxFileSize: Int) -> SignalAttachment {
|
|
|
|
|
assert(dataUTI.characters.count > 0)
|
|
|
|
|
|
|
|
|
|
assert(data != nil)
|
|
|
|
|
guard let data = data else {
|
|
|
|
|
return nil
|
|
|
|
|
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
|
|
|
|
|
attachment.error = .missingData
|
|
|
|
|
return attachment
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let attachment = SignalAttachment(data : data, dataUTI: dataUTI)
|
|
|
|
|
|
|
|
|
|
if validUTISet != nil {
|
|
|
|
|
guard validUTISet!.contains(dataUTI) else {
|
|
|
|
|
if let validUTISet = validUTISet {
|
|
|
|
|
guard validUTISet.contains(dataUTI) else {
|
|
|
|
|
attachment.error = .invalidFileFormat
|
|
|
|
|
return attachment
|
|
|
|
|
}
|
|
|
|
|