|
|
@ -51,18 +51,23 @@ class SignalAttachment: NSObject {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// To avoid redundant work of repeatedly compressing/uncompressing
|
|
|
|
|
|
|
|
// images, we cache the UIImage associated with this attachment if
|
|
|
|
|
|
|
|
// possible.
|
|
|
|
|
|
|
|
public var image: UIImage?
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: Constants
|
|
|
|
// MARK: Constants
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Media Size constraints from Signal-Android
|
|
|
|
* Media Size constraints from Signal-Android
|
|
|
|
* (org/thoughtcrime/securesms/mms/PushMediaConstraints.java)
|
|
|
|
* (org/thoughtcrime/securesms/mms/PushMediaConstraints.java)
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
static let kMaxFileSize_Gif = 5 * 1024 * 1024
|
|
|
|
static let kMaxFileSizeGif = 5 * 1024 * 1024
|
|
|
|
static let kMaxFileSize_Image = 420 * 1024
|
|
|
|
static let kMaxFileSizeImage = 420 * 1024
|
|
|
|
static let kMaxFileSize_Video = 100 * 1024 * 1024
|
|
|
|
static let kMaxFileSizeVideo = 100 * 1024 * 1024
|
|
|
|
static let kMaxFileSize_Audio = 100 * 1024 * 1024
|
|
|
|
static let kMaxFileSizeAudio = 100 * 1024 * 1024
|
|
|
|
// TODO: What should the max file size on "other" attachments be?
|
|
|
|
// TODO: What should the max file size on "other" attachments be?
|
|
|
|
static let kMaxFileSize_Generic = 25 * 1024 * 1024
|
|
|
|
static let kMaxFileSizeGeneric = 100 * 1024 * 1024
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: Constructor
|
|
|
|
// MARK: Constructor
|
|
|
|
|
|
|
|
|
|
|
@ -88,6 +93,16 @@ class SignalAttachment: NSObject {
|
|
|
|
return mimeType?.takeRetainedValue() as? String
|
|
|
|
return mimeType?.takeRetainedValue() as? String
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return fileExtension?.takeRetainedValue() as? String
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Returns the set of UTIs that correspond to valid _input_ image formats
|
|
|
|
// Returns the set of UTIs that correspond to valid _input_ image formats
|
|
|
|
// for Signal attachments.
|
|
|
|
// for Signal attachments.
|
|
|
|
//
|
|
|
|
//
|
|
|
@ -99,7 +114,7 @@ class SignalAttachment: NSObject {
|
|
|
|
return [
|
|
|
|
return [
|
|
|
|
kUTTypeJPEG as String,
|
|
|
|
kUTTypeJPEG as String,
|
|
|
|
kUTTypeGIF as String,
|
|
|
|
kUTTypeGIF as String,
|
|
|
|
kUTTypePNG as String,
|
|
|
|
kUTTypePNG as String
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -111,7 +126,7 @@ class SignalAttachment: NSObject {
|
|
|
|
return [
|
|
|
|
return [
|
|
|
|
kUTTypeJPEG as String,
|
|
|
|
kUTTypeJPEG as String,
|
|
|
|
kUTTypeGIF as String,
|
|
|
|
kUTTypeGIF as String,
|
|
|
|
kUTTypePNG as String,
|
|
|
|
kUTTypePNG as String
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -121,7 +136,7 @@ class SignalAttachment: NSObject {
|
|
|
|
// TODO: We need to finalize which formats we support.
|
|
|
|
// TODO: We need to finalize which formats we support.
|
|
|
|
private class func videoUTISet() -> Set<String>! {
|
|
|
|
private class func videoUTISet() -> Set<String>! {
|
|
|
|
return [
|
|
|
|
return [
|
|
|
|
kUTTypeMPEG4 as String,
|
|
|
|
kUTTypeMPEG4 as String
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -132,7 +147,7 @@ class SignalAttachment: NSObject {
|
|
|
|
private class func audioUTISet() -> Set<String>! {
|
|
|
|
private class func audioUTISet() -> Set<String>! {
|
|
|
|
return [
|
|
|
|
return [
|
|
|
|
kUTTypeMP3 as String,
|
|
|
|
kUTTypeMP3 as String,
|
|
|
|
kUTTypeMPEG4Audio as String,
|
|
|
|
kUTTypeMPEG4Audio as String
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -142,6 +157,18 @@ class SignalAttachment: NSObject {
|
|
|
|
return inputImageUTISet().union(videoUTISet().union(audioUTISet()))
|
|
|
|
return inputImageUTISet().union(videoUTISet().union(audioUTISet()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public func isImage() -> Bool {
|
|
|
|
|
|
|
|
return SignalAttachment.outputImageUTISet().contains(dataUTI)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public func isVideo() -> Bool {
|
|
|
|
|
|
|
|
return SignalAttachment.videoUTISet().contains(dataUTI)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public func isAudio() -> Bool {
|
|
|
|
|
|
|
|
return SignalAttachment.audioUTISet().contains(dataUTI)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Returns an attachment from the pasteboard, or nil if no attachment
|
|
|
|
// Returns an attachment from the pasteboard, or nil if no attachment
|
|
|
|
// can be found.
|
|
|
|
// can be found.
|
|
|
|
//
|
|
|
|
//
|
|
|
@ -206,7 +233,7 @@ class SignalAttachment: NSObject {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if dataUTI == kUTTypeGIF as String {
|
|
|
|
if dataUTI == kUTTypeGIF as String {
|
|
|
|
guard imageData.count <= kMaxFileSize_Gif else {
|
|
|
|
guard imageData.count <= kMaxFileSizeGif else {
|
|
|
|
attachment.error = .fileSizeTooLarge
|
|
|
|
attachment.error = .fileSizeTooLarge
|
|
|
|
return attachment
|
|
|
|
return attachment
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -214,33 +241,46 @@ class SignalAttachment: NSObject {
|
|
|
|
// animated.
|
|
|
|
// animated.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// TODO: Consider re-encoding non-animated GIFs as JPEG?
|
|
|
|
// TODO: Consider re-encoding non-animated GIFs as JPEG?
|
|
|
|
Logger.verbose("\(TAG) Sending raw image/gif to retain any animation")
|
|
|
|
Logger.verbose("\(TAG) Sending raw \(attachment.mimeType()) to retain any animation")
|
|
|
|
return attachment
|
|
|
|
return attachment
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
guard let image = UIImage(data:imageData) else {
|
|
|
|
guard let image = UIImage(data:imageData) else {
|
|
|
|
attachment.error = .couldNotParseImage
|
|
|
|
attachment.error = .couldNotParseImage
|
|
|
|
return attachment
|
|
|
|
return attachment
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
attachment.image = image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if isInputImageValidOutputImage(image: image, imageData: imageData, dataUTI: dataUTI) {
|
|
|
|
|
|
|
|
Logger.verbose("\(TAG) Sending raw \(attachment.mimeType())")
|
|
|
|
|
|
|
|
return attachment
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Logger.verbose("\(TAG) Compressing attachment as image/jpeg")
|
|
|
|
|
|
|
|
return compressImageAsJPEG(image : image, attachment : attachment)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If the proposed attachment already is a JPEG,
|
|
|
|
// If the proposed attachment already is a JPEG, and already conforms to the
|
|
|
|
// and already conforms to the file size and
|
|
|
|
// file size and content size limits, don't recompress it.
|
|
|
|
// content size limits, don't recompress it.
|
|
|
|
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// TODO: Should non-JPEGs always be converted to JPEG?
|
|
|
|
// TODO: Should non-JPEGs always be converted to JPEG?
|
|
|
|
|
|
|
|
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 {
|
|
|
|
if dataUTI == kUTTypeJPEG as String {
|
|
|
|
let imageUploadQuality = Environment.preferences().imageUploadQuality()
|
|
|
|
let imageUploadQuality = Environment.preferences().imageUploadQuality()
|
|
|
|
let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
|
|
|
|
let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
|
|
|
|
if (image.size.width <= maxSize &&
|
|
|
|
if image.size.width <= maxSize &&
|
|
|
|
image.size.height <= maxSize &&
|
|
|
|
image.size.height <= maxSize &&
|
|
|
|
imageData.count <= kMaxFileSize_Image) {
|
|
|
|
imageData.count <= kMaxFileSizeImage {
|
|
|
|
Logger.verbose("\(TAG) Sending raw image/jpeg")
|
|
|
|
return true
|
|
|
|
return attachment
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
Logger.verbose("\(TAG) Converting attachment to image/jpeg")
|
|
|
|
|
|
|
|
return recompressImageAsJPEG(image : image, attachment : attachment)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Factory method for an image attachment.
|
|
|
|
// Factory method for an image attachment.
|
|
|
@ -256,27 +296,34 @@ class SignalAttachment: NSObject {
|
|
|
|
|
|
|
|
|
|
|
|
// Make a placeholder attachment on which to hang errors if necessary.
|
|
|
|
// Make a placeholder attachment on which to hang errors if necessary.
|
|
|
|
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
|
|
|
|
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
|
|
|
|
|
|
|
|
attachment.image = image
|
|
|
|
|
|
|
|
|
|
|
|
Logger.verbose("\(TAG) Converting attachment to image/jpeg")
|
|
|
|
Logger.verbose("\(TAG) Writing \(attachment.mimeType()) as image/jpeg")
|
|
|
|
return recompressImageAsJPEG(image : image, attachment : attachment)
|
|
|
|
return compressImageAsJPEG(image : image, attachment : attachment)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private class func recompressImageAsJPEG(image : UIImage!, attachment : SignalAttachment!) -> SignalAttachment! {
|
|
|
|
private class func compressImageAsJPEG(image: UIImage!, attachment: SignalAttachment!) -> SignalAttachment! {
|
|
|
|
assert(attachment.error == nil)
|
|
|
|
assert(attachment.error == nil)
|
|
|
|
|
|
|
|
|
|
|
|
var imageUploadQuality = Environment.preferences().imageUploadQuality()
|
|
|
|
var imageUploadQuality = Environment.preferences().imageUploadQuality()
|
|
|
|
|
|
|
|
|
|
|
|
while true {
|
|
|
|
while true {
|
|
|
|
let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
|
|
|
|
let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
|
|
|
|
let resizedImage = imageScaled(image, toMaxSize: maxSize)
|
|
|
|
var dstImage: UIImage! = image
|
|
|
|
guard let jpgImageData = UIImageJPEGRepresentation(resizedImage,
|
|
|
|
if image.size.width > maxSize ||
|
|
|
|
|
|
|
|
image.size.height > maxSize {
|
|
|
|
|
|
|
|
dstImage = imageScaled(image, toMaxSize: maxSize)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let jpgImageData = UIImageJPEGRepresentation(dstImage,
|
|
|
|
jpegCompressionQuality(imageUploadQuality:imageUploadQuality)) else {
|
|
|
|
jpegCompressionQuality(imageUploadQuality:imageUploadQuality)) else {
|
|
|
|
attachment.error = .couldNotConvertToJpeg
|
|
|
|
attachment.error = .couldNotConvertToJpeg
|
|
|
|
return attachment
|
|
|
|
return attachment
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if jpgImageData.count <= kMaxFileSize_Image {
|
|
|
|
if jpgImageData.count <= kMaxFileSizeImage {
|
|
|
|
return SignalAttachment(data : jpgImageData, dataUTI: kUTTypeJPEG as String)
|
|
|
|
let recompressedAttachment = SignalAttachment(data : jpgImageData, dataUTI: kUTTypeJPEG as String)
|
|
|
|
|
|
|
|
recompressedAttachment.image = dstImage
|
|
|
|
|
|
|
|
return recompressedAttachment
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If the JPEG output is larger than the file size limit,
|
|
|
|
// If the JPEG output is larger than the file size limit,
|
|
|
@ -301,8 +348,7 @@ class SignalAttachment: NSObject {
|
|
|
|
let aspectRatio: CGFloat = image.size.height / image.size.width
|
|
|
|
let aspectRatio: CGFloat = image.size.height / image.size.width
|
|
|
|
if aspectRatio > 1 {
|
|
|
|
if aspectRatio > 1 {
|
|
|
|
scaleFactor = size / image.size.width
|
|
|
|
scaleFactor = size / image.size.width
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
else {
|
|
|
|
|
|
|
|
scaleFactor = size / image.size.height
|
|
|
|
scaleFactor = size / image.size.height
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let newSize = CGSize(width: CGFloat(image.size.width * scaleFactor), height: CGFloat(image.size.height * scaleFactor))
|
|
|
|
let newSize = CGSize(width: CGFloat(image.size.width * scaleFactor), height: CGFloat(image.size.height * scaleFactor))
|
|
|
@ -349,7 +395,7 @@ class SignalAttachment: NSObject {
|
|
|
|
return newAttachment(withData : data,
|
|
|
|
return newAttachment(withData : data,
|
|
|
|
dataUTI : dataUTI,
|
|
|
|
dataUTI : dataUTI,
|
|
|
|
validUTISet : videoUTISet(),
|
|
|
|
validUTISet : videoUTISet(),
|
|
|
|
maxFileSize : kMaxFileSize_Video)
|
|
|
|
maxFileSize : kMaxFileSizeVideo)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: Audio Attachments
|
|
|
|
// MARK: Audio Attachments
|
|
|
@ -362,7 +408,7 @@ class SignalAttachment: NSObject {
|
|
|
|
return newAttachment(withData : data,
|
|
|
|
return newAttachment(withData : data,
|
|
|
|
dataUTI : dataUTI,
|
|
|
|
dataUTI : dataUTI,
|
|
|
|
validUTISet : audioUTISet(),
|
|
|
|
validUTISet : audioUTISet(),
|
|
|
|
maxFileSize : kMaxFileSize_Audio)
|
|
|
|
maxFileSize : kMaxFileSizeAudio)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: Generic Attachments
|
|
|
|
// MARK: Generic Attachments
|
|
|
@ -375,7 +421,7 @@ class SignalAttachment: NSObject {
|
|
|
|
return newAttachment(withData : data,
|
|
|
|
return newAttachment(withData : data,
|
|
|
|
dataUTI : dataUTI,
|
|
|
|
dataUTI : dataUTI,
|
|
|
|
validUTISet : nil,
|
|
|
|
validUTISet : nil,
|
|
|
|
maxFileSize : kMaxFileSize_Generic)
|
|
|
|
maxFileSize : kMaxFileSizeGeneric)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: Helper Methods
|
|
|
|
// MARK: Helper Methods
|
|
|
|