//
// C o p y r i g h t ( c ) 2 0 1 8 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
import Foundation
import MobileCoreServices
import SignalServiceKit
import PromiseKit
import AVFoundation
enum SignalAttachmentError : Error {
case missingData
case fileSizeTooLarge
case invalidData
case couldNotParseImage
case couldNotConvertToJpeg
case couldNotConvertToMpeg4
case invalidFileFormat
}
extension String {
var filenameWithoutExtension : String {
return ( self as NSString ) . deletingPathExtension
}
var fileExtension : String ? {
return ( self as NSString ) . pathExtension
}
func appendingFileExtension ( _ fileExtension : String ) -> String {
guard let result = ( self as NSString ) . appendingPathExtension ( fileExtension ) else {
owsFail ( " Failed to append file extension: \( fileExtension ) to string: \( self ) " )
return self
}
return result
}
}
extension SignalAttachmentError : LocalizedError {
public var errorDescription : String {
switch self {
case . missingData :
return NSLocalizedString ( " ATTACHMENT_ERROR_MISSING_DATA " , comment : " Attachment error message for attachments without any data " )
case . fileSizeTooLarge :
return NSLocalizedString ( " ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE " , comment : " Attachment error message for attachments whose data exceed file size limits " )
case . invalidData :
return NSLocalizedString ( " ATTACHMENT_ERROR_INVALID_DATA " , comment : " Attachment error message for attachments with invalid data " )
case . couldNotParseImage :
return NSLocalizedString ( " ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE " , comment : " Attachment error message for image attachments which cannot be parsed " )
case . couldNotConvertToJpeg :
return NSLocalizedString ( " ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG " , comment : " Attachment error message for image attachments which could not be converted to JPEG " )
case . invalidFileFormat :
return NSLocalizedString ( " ATTACHMENT_ERROR_INVALID_FILE_FORMAT " , comment : " Attachment error message for attachments with an invalid file format " )
case . couldNotConvertToMpeg4 :
return NSLocalizedString ( " ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4 " , comment : " Attachment error message for video attachments which could not be converted to MP4 " )
}
}
}
@objc
public enum TSImageQualityTier : UInt {
case original
case high
case mediumHigh
case medium
case mediumLow
case low
}
@objc
public enum TSImageQuality : UInt {
case original
case medium
case compact
func imageQualityTier ( ) -> TSImageQualityTier {
switch self {
case . original :
return . original
case . medium :
return . mediumHigh
case . compact :
return . medium
}
}
}
// R e p r e s e n t s a p o s s i b l e a t t a c h m e n t t o u p l o a d .
// T h e a t t a c h m e n t m a y b e i n v a l i d .
//
// S i g n a l a t t a c h m e n t s a r e s u b j e c t t o v a l i d a t i o n a n d
// i n s o m e c a s e s , f i l e f o r m a t c o n v e r s i o n .
//
// T h i s c l a s s g a t h e r s t h a t l o g i c . I t o f f e r s f a c t o r y m e t h o d s
// f o r a t t a c h m e n t s t h a t d o t h e n e c e s s a r y w o r k .
//
// T h e r e t u r n v a l u e f o r t h e f a c t o r y m e t h o d s w i l l b e n i l i f t h e i n p u t i s n i l .
//
// [ S i g n a l A t t a c h m e n t h a s E r r o r ] w i l l b e t r u e f o r n o n - v a l i d a t t a c h m e n t s .
//
// TODO: P e r h a p s d o c o n v e r s i o n o f f t h e m a i n t h r e a d ?
@objc
public class SignalAttachment : NSObject {
static let TAG = " [SignalAttachment] "
let TAG = " [SignalAttachment] "
// MARK: P r o p e r t i e s
@objc
public let dataSource : DataSource
@objc
public var captionText : String ?
@objc
public var data : Data {
return dataSource . data ( )
}
@objc
public var dataLength : UInt {
return dataSource . dataLength ( )
}
@objc
public var dataUrl : URL ? {
return dataSource . dataUrl ( )
}
@objc
public var sourceFilename : String ? {
return dataSource . sourceFilename
}
@objc
public var isValidImage : Bool {
return dataSource . isValidImage ( )
}
// T h i s f l a g s h o u l d b e s e t f o r t e x t a t t a c h m e n t s t h a t c a n b e s e n t a s t e x t m e s s a g e s .
@objc
public var isConvertibleToTextMessage = false
// A t t a c h m e n t t y p e s a r e i d e n t i f i e d u s i n g U T I s .
//
// S e e : h t t p s : / / d e v e l o p e r . a p p l e . c o m / l i b r a r y / c o n t e n t / d o c u m e n t a t i o n / M i s c e l l a n e o u s / R e f e r e n c e / U T I R e f / A r t i c l e s / S y s t e m - D e c l a r e d U n i f o r m T y p e I d e n t i f i e r s . h t m l
@objc
public let dataUTI : String
var error : SignalAttachmentError ? {
didSet {
AssertIsOnMainThread ( )
assert ( oldValue = = nil )
Logger . verbose ( " \( SignalAttachment . TAG ) Attachment has error: \( String ( describing : error ) ) " )
}
}
// T o a v o i d r e d u n d a n t w o r k o f r e p e a t e d l y c o m p r e s s i n g / u n c o m p r e s s i n g
// i m a g e s , w e c a c h e t h e U I I m a g e a s s o c i a t e d w i t h t h i s a t t a c h m e n t i f
// p o s s i b l e .
private var cachedImage : UIImage ?
private var cachedVideoPreview : UIImage ?
@objc
private ( set ) public var isVoiceMessage = false
// MARK: C o n s t a n t s
/* *
* Media Size constraints from Signal - Android
*
* https : // g i t h u b . c o m / s i g n a l a p p / S i g n a l - A n d r o i d / b l o b / m a s t e r / s r c / o r g / t h o u g h t c r i m e / s e c u r e s m s / m m s / P u s h M e d i a C o n s t r a i n t s . j a v a
*/
static let kMaxFileSizeAnimatedImage = UInt ( 25 * 1024 * 1024 )
static let kMaxFileSizeImage = UInt ( 6 * 1024 * 1024 )
static let kMaxFileSizeVideo = UInt ( 100 * 1024 * 1024 )
static let kMaxFileSizeAudio = UInt ( 100 * 1024 * 1024 )
static let kMaxFileSizeGeneric = UInt ( 100 * 1024 * 1024 )
// MARK: C o n s t r u c t o r
// T h i s m e t h o d s h o u l d n o t b e c a l l e d d i r e c t l y ; u s e t h e f a c t o r y
// m e t h o d s i n s t e a d .
@objc
private init ( dataSource : DataSource , dataUTI : String ) {
self . dataSource = dataSource
self . dataUTI = dataUTI
super . init ( )
}
// MARK: M e t h o d s
@objc
public var hasError : Bool {
return error != nil
}
@objc
public var errorName : String ? {
guard let error = error else {
// T h i s m e t h o d s h o u l d o n l y b e c a l l e d i f t h e r e i s a n e r r o r .
owsFail ( " \( TAG ) Missing error " )
return nil
}
return " \( error ) "
}
@objc
public var localizedErrorDescription : String ? {
guard let error = self . error else {
// T h i s m e t h o d s h o u l d o n l y b e c a l l e d i f t h e r e i s a n e r r o r .
owsFail ( " \( TAG ) Missing error " )
return nil
}
return " \( error . errorDescription ) "
}
@objc
public class var missingDataErrorMessage : String {
return SignalAttachmentError . missingData . errorDescription
}
@objc
public func image ( ) -> UIImage ? {
if let cachedImage = cachedImage {
return cachedImage
}
guard let image = UIImage ( data : dataSource . data ( ) ) else {
return nil
}
cachedImage = image
return image
}
@objc
public func videoPreview ( ) -> UIImage ? {
if let cachedVideoPreview = cachedVideoPreview {
return cachedVideoPreview
}
guard let mediaUrl = dataUrl else {
return nil
}
do {
let filePath = mediaUrl . path
guard FileManager . default . fileExists ( atPath : filePath ) else {
owsFail ( " asset at \( filePath ) doesn't exist " )
return nil
}
let asset = AVURLAsset ( url : mediaUrl )
let generator = AVAssetImageGenerator ( asset : asset )
generator . appliesPreferredTrackTransform = true
let cgImage = try generator . copyCGImage ( at : CMTimeMake ( 0 , 1 ) , actualTime : nil )
let image = UIImage ( cgImage : cgImage )
cachedVideoPreview = image
return image
} catch let error {
Logger . verbose ( " \( TAG ) Could not generate video thumbnail: \( error . localizedDescription ) " )
return nil
}
}
// R e t u r n s t h e M I M E t y p e f o r t h i s a t t a c h m e n t o r n i l i f n o M I M E t y p e
// c a n b e i d e n t i f i e d .
@objc
public var mimeType : String {
if isVoiceMessage {
// L e g a c y i O S c l i e n t s d o n ' t h a n d l e " a u d i o / m p 4 " f i l e s c o r r e c t l y ;
// t h e y a r e w r i t t e n t o d i s k a s . m p 4 i n s t e a d o f . m 4 a w h i c h b r e a k s
// p l a y b a c k . S o w e s e n d v o i c e m e s s a g e s a s " a u d i o / a a c " t o w o r k
// a r o u n d t h i s .
//
// TODO: R e m o v e t h i s N o v . 2 0 1 6 o r a f t e r .
return " audio/aac "
}
if let filename = sourceFilename {
let fileExtension = ( filename as NSString ) . pathExtension
if fileExtension . count > 0 {
if let mimeType = MIMETypeUtil . mimeType ( forFileExtension : fileExtension ) {
// U T I t y p e s a r e a n i m p e r f e c t m e a n s o f r e p r e s e n t i n g f i l e t y p e ;
// f i l e e x t e n s i o n s a r e a l s o i m p e r f e c t b u t f a r m o r e r e l i a b l e a n d
// c o m p r e h e n s i v e s o w e a l w a y s p r e f e r t o t r y t o d e d u c e M I M E t y p e
// f r o m t h e f i l e e x t e n s i o n .
return mimeType
}
}
}
if isOversizeText {
return OWSMimeTypeOversizeTextMessage
}
if dataUTI = = kUnknownTestAttachmentUTI {
return OWSMimeTypeUnknownForTests
}
guard let mimeType = UTTypeCopyPreferredTagWithClass ( dataUTI as CFString , kUTTagClassMIMEType ) else {
return OWSMimeTypeApplicationOctetStream
}
return mimeType . takeRetainedValue ( ) as String
}
// U s e t h e f i l e n a m e i f k n o w n . I f n o t , e . g . i f t h e a t t a c h m e n t w a s c o p y / p a s t e d , w e ' l l g e n e r a t e a f i l e n a m e
// l i k e : " s i g n a l - 2 0 1 7 - 0 4 - 2 4 - 0 9 5 9 1 8 . z i p "
@objc
public var filenameOrDefault : String {
if let filename = sourceFilename {
return filename
} else {
let kDefaultAttachmentName = " signal "
let dateFormatter = DateFormatter ( )
dateFormatter . dateFormat = " YYYY-MM-dd-HHmmss "
let dateString = dateFormatter . string ( from : Date ( ) )
let withoutExtension = " \( kDefaultAttachmentName ) - \( dateString ) "
if let fileExtension = self . fileExtension {
return " \( withoutExtension ) . \( fileExtension ) "
}
return withoutExtension
}
}
// R e t u r n s t h e f i l e e x t e n s i o n f o r t h i s a t t a c h m e n t o r n i l i f n o f i l e e x t e n s i o n
// c a n b e i d e n t i f i e d .
@objc
public var fileExtension : String ? {
if let filename = sourceFilename {
let fileExtension = ( filename as NSString ) . pathExtension
if fileExtension . count > 0 {
return fileExtension
}
}
if isOversizeText {
return kOversizeTextAttachmentFileExtension
}
if dataUTI = = kUnknownTestAttachmentUTI {
return " unknown "
}
guard let fileExtension = MIMETypeUtil . fileExtension ( forUTIType : dataUTI ) else {
return nil
}
return fileExtension
}
// R e t u r n s t h e s e t o f U T I s t h a t c o r r e s p o n d t o v a l i d _ i n p u t _ i m a g e f o r m a t s
// f o r S i g n a l a t t a c h m e n t s .
//
// I m a g e a t t a c h m e n t s m a y b e c o n v e r t e d t o a n o t h e r i m a g e f o r m a t b e f o r e
// b e i n g u p l o a d e d .
private class var inputImageUTISet : Set < String > {
// H E I C i s v a l i d i n p u t , b u t n o t v a l i d o u t p u t . N o n - i O S 1 1 c l i e n t s d o n o t s u p p o r t i t .
let heicSet : Set < String > = Set ( [ " public.heic " , " public.heif " ] )
return MIMETypeUtil . supportedImageUTITypes ( )
. union ( animatedImageUTISet )
. union ( heicSet )
}
// R e t u r n s t h e s e t o f U T I s t h a t c o r r e s p o n d t o v a l i d _ o u t p u t _ i m a g e f o r m a t s
// f o r S i g n a l a t t a c h m e n t s .
private class var outputImageUTISet : Set < String > {
return MIMETypeUtil . supportedImageUTITypes ( ) . union ( animatedImageUTISet )
}
private class var outputVideoUTISet : Set < String > {
return Set ( [ kUTTypeMPEG4 as String ] )
}
// R e t u r n s t h e s e t o f U T I s t h a t c o r r e s p o n d t o v a l i d a n i m a t e d i m a g e f o r m a t s
// f o r S i g n a l a t t a c h m e n t s .
private class var animatedImageUTISet : Set < String > {
return MIMETypeUtil . supportedAnimatedImageUTITypes ( )
}
// R e t u r n s t h e s e t o f U T I s t h a t c o r r e s p o n d t o v a l i d v i d e o f o r m a t s
// f o r S i g n a l a t t a c h m e n t s .
private class var videoUTISet : Set < String > {
return MIMETypeUtil . supportedVideoUTITypes ( )
}
// R e t u r n s t h e s e t o f U T I s t h a t c o r r e s p o n d t o v a l i d a u d i o f o r m a t s
// f o r S i g n a l a t t a c h m e n t s .
private class var audioUTISet : Set < String > {
return MIMETypeUtil . supportedAudioUTITypes ( )
}
// R e t u r n s t h e s e t o f U T I s t h a t c o r r e s p o n d t o v a l i d i m a g e , v i d e o a n d a u d i o f o r m a t s
// f o r S i g n a l a t t a c h m e n t s .
private class var mediaUTISet : Set < String > {
return audioUTISet . union ( videoUTISet ) . union ( animatedImageUTISet ) . union ( inputImageUTISet )
}
@objc
public var isImage : Bool {
return SignalAttachment . outputImageUTISet . contains ( dataUTI )
}
@objc
public var isAnimatedImage : Bool {
return SignalAttachment . animatedImageUTISet . contains ( dataUTI )
}
@objc
public var isVideo : Bool {
return SignalAttachment . videoUTISet . contains ( dataUTI )
}
@objc
public var isAudio : Bool {
return SignalAttachment . audioUTISet . contains ( dataUTI )
}
@objc
public var isOversizeText : Bool {
return dataUTI = = kOversizeTextAttachmentUTI
}
@objc
public var isText : Bool {
return UTTypeConformsTo ( dataUTI as CFString , kUTTypeText ) || isOversizeText
}
@objc
public var isUrl : Bool {
return UTTypeConformsTo ( dataUTI as CFString , kUTTypeURL )
}
@objc
public class func pasteboardHasPossibleAttachment ( ) -> Bool {
return UIPasteboard . general . numberOfItems > 0
}
@objc
public class func pasteboardHasText ( ) -> Bool {
if UIPasteboard . general . numberOfItems < 1 {
return false
}
let itemSet = IndexSet ( integer : 0 )
guard let pasteboardUTITypes = UIPasteboard . general . types ( forItemSet : itemSet ) else {
return false
}
let pasteboardUTISet = Set < String > ( pasteboardUTITypes [ 0 ] )
// T h e p a s t e b o a r d c a n b e p o p u l a t e d w i t h m u l t i p l e U T I t y p e s
// w i t h d i f f e r e n t p a y l o a d s . i M e s s a g e f o r e x a m p l e w i l l c o p y
// a n a n i m a t e d G I F t o t h e p a s t e b o a r d w i t h t h e f o l l o w i n g U T I
// t y p e s :
//
// * " p u b l i c . u r l - n a m e "
// * " p u b l i c . u t f 8 - p l a i n - t e x t "
// * " c o m . c o m p u s e r v e . g i f "
//
// W e w a n t t o p a s t e t h e a n i m a t e d G I F i t s e l f , n o t i t ' s n a m e .
//
// I n g e n e r a l , o u r r u l e i s t o p r e f e r n o n - t e x t p a s t e b o a r d
// c o n t e n t s , s o w e r e t u r n t r u e I F F t h e r e i s a t e x t U T I t y p e
// a n d t h e r e i s n o n o n - t e x t U T I t y p e .
var hasTextUTIType = false
var hasNonTextUTIType = false
for utiType in pasteboardUTISet {
if UTTypeConformsTo ( utiType as CFString , kUTTypeText ) {
hasTextUTIType = true
} else if mediaUTISet . contains ( utiType ) {
hasNonTextUTIType = true
}
}
if pasteboardUTISet . contains ( kUTTypeURL as String ) {
// T r e a t U R L a s a t e x t u a l U T I t y p e .
hasTextUTIType = true
}
if hasNonTextUTIType {
return false
}
return hasTextUTIType
}
// R e t u r n s a n a t t a c h m e n t f r o m t h e p a s t e b o a r d , o r n i l i f n o a t t a c h m e n t
// c a n b e f o u n d .
//
// N O T E : T h e a t t a c h m e n t r e t u r n e d b y t h i s m e t h o d m a y n o t b e v a l i d .
// C h e c k t h e a t t a c h m e n t ' s e r r o r p r o p e r t y .
@objc
public class func attachmentFromPasteboard ( ) -> SignalAttachment ? {
guard UIPasteboard . general . numberOfItems >= 1 else {
return nil
}
// I f p a s t e b o a r d c o n t a i n s m u l t i p l e i t e m s , u s e o n l y t h e f i r s t .
let itemSet = IndexSet ( integer : 0 )
guard let pasteboardUTITypes = UIPasteboard . general . types ( forItemSet : itemSet ) else {
return nil
}
let pasteboardUTISet = Set < String > ( pasteboardUTITypes [ 0 ] )
for dataUTI in inputImageUTISet {
if pasteboardUTISet . contains ( dataUTI ) {
guard let data = dataForFirstPasteboardItem ( dataUTI : dataUTI ) else {
owsFail ( " \( TAG ) Missing expected pasteboard data for UTI: \( dataUTI ) " )
return nil
}
let dataSource = DataSourceValue . dataSource ( with : data , utiType : dataUTI )
// P a s t e d i m a g e s _ S H O U L D _ N O T _ b e r e s i z e d , i f p o s s i b l e .
return attachment ( dataSource : dataSource , dataUTI : dataUTI , imageQuality : . medium )
}
}
for dataUTI in videoUTISet {
if pasteboardUTISet . contains ( dataUTI ) {
guard let data = dataForFirstPasteboardItem ( dataUTI : dataUTI ) else {
owsFail ( " \( TAG ) Missing expected pasteboard data for UTI: \( dataUTI ) " )
return nil
}
let dataSource = DataSourceValue . dataSource ( with : data , utiType : dataUTI )
return videoAttachment ( dataSource : dataSource , dataUTI : dataUTI )
}
}
for dataUTI in audioUTISet {
if pasteboardUTISet . contains ( dataUTI ) {
guard let data = dataForFirstPasteboardItem ( dataUTI : dataUTI ) else {
owsFail ( " \( TAG ) Missing expected pasteboard data for UTI: \( dataUTI ) " )
return nil
}
let dataSource = DataSourceValue . dataSource ( with : data , utiType : dataUTI )
return audioAttachment ( dataSource : dataSource , dataUTI : dataUTI )
}
}
let dataUTI = pasteboardUTISet [ pasteboardUTISet . startIndex ]
guard let data = dataForFirstPasteboardItem ( dataUTI : dataUTI ) else {
owsFail ( " \( TAG ) Missing expected pasteboard data for UTI: \( dataUTI ) " )
return nil
}
let dataSource = DataSourceValue . dataSource ( with : data , utiType : dataUTI )
return genericAttachment ( dataSource : dataSource , dataUTI : dataUTI )
}
// T h i s m e t h o d s h o u l d o n l y b e c a l l e d f o r d a t a U T I s t h a t
// a r e a p p r o p r i a t e f o r t h e f i r s t p a s t e b o a r d i t e m .
private class func dataForFirstPasteboardItem ( dataUTI : String ) -> Data ? {
let itemSet = IndexSet ( integer : 0 )
guard let datas = UIPasteboard . general . data ( forPasteboardType : dataUTI , inItemSet : itemSet ) else {
owsFail ( " \( TAG ) Missing expected pasteboard data for UTI: \( dataUTI ) " )
return nil
}
guard datas . count > 0 else {
owsFail ( " \( TAG ) Missing expected pasteboard data for UTI: \( dataUTI ) " )
return nil
}
guard let data = datas [ 0 ] as ? Data else {
owsFail ( " \( TAG ) Missing expected pasteboard data for UTI: \( dataUTI ) " )
return nil
}
return data
}
// MARK: I m a g e A t t a c h m e n t s
// F a c t o r y m e t h o d f o r a n i m a g e a t t a c h m e n t .
//
// N O T E : T h e a t t a c h m e n t r e t u r n e d b y t h i s m e t h o d m a y n o t b e v a l i d .
// C h e c k t h e a t t a c h m e n t ' s e r r o r p r o p e r t y .
@objc
private class func imageAttachment ( dataSource : DataSource ? , dataUTI : String , imageQuality : TSImageQuality ) -> SignalAttachment {
assert ( dataUTI . count > 0 )
assert ( dataSource != nil )
guard let dataSource = dataSource else {
let attachment = SignalAttachment ( dataSource : DataSourceValue . emptyDataSource ( ) , dataUTI : dataUTI )
attachment . error = . missingData
return attachment
}
let attachment = SignalAttachment ( dataSource : dataSource , dataUTI : dataUTI )
guard inputImageUTISet . contains ( dataUTI ) else {
attachment . error = . invalidFileFormat
return attachment
}
guard dataSource . dataLength ( ) > 0 else {
owsFail ( " \( self . TAG ) in \( #function ) imageData was empty " )
attachment . error = . invalidData
return attachment
}
if animatedImageUTISet . contains ( dataUTI ) {
guard dataSource . dataLength ( ) <= kMaxFileSizeAnimatedImage else {
attachment . error = . fileSizeTooLarge
return attachment
}
// N e v e r r e - e n c o d e a n i m a t e d i m a g e s ( i . e . G I F s ) a s J P E G s .
Logger . verbose ( " \( TAG ) Sending raw \( attachment . mimeType ) to retain any animation " )
return attachment
} else {
guard let image = UIImage ( data : dataSource . data ( ) ) else {
attachment . error = . couldNotParseImage
return attachment
}
attachment . cachedImage = image
if isValidOutputImage ( image : image , dataSource : dataSource , dataUTI : dataUTI , imageQuality : imageQuality ) {
if let sourceFilename = dataSource . sourceFilename ,
let sourceFileExtension = sourceFilename . fileExtension ,
[ " heic " , " heif " ] . contains ( sourceFileExtension . lowercased ( ) ) {
// I f a . h e i c f i l e a c t u a l l y c o n t a i n s j p e g d a t a , u p d a t e t h e e x t e n s i o n t o m a t c h .
//
// H e r e ' s h o w t h a t c a n h a p p e n :
// I n i O S 1 1 , t h e P h o t o s . a p p r e c o r d s p h o t o s w i t h H E I C U T I T y p e , w i t h t h e . H E I C e x t e n s i o n .
// S i n c e H E I C i s n ' t a v a l i d o u t p u t f o r m a t f o r S i g n a l , w e ' l l d e t e c t t h a t a n d c o n v e r t t o J P E G ,
// u p d a t i n g t h e e x t e n s i o n a s w e l l . N o p r o b l e m .
// H o w e v e r t h e p r o b l e m c o m e s i n w h e n y o u e d i t a n H E I C i m a g e i n P h o t o s . a p p - t h e i m a g e i s s a v e d
// i n t h e P h o t o s . a p p a s a J P E G , b u t r e t a i n s t h e ( n o w i n c o n g r u o u s ) H E I C e x t e n s i o n i n t h e f i l e n a m e .
assert ( dataUTI = = kUTTypeJPEG as String )
Logger . verbose ( " \( self . TAG ) changing extension: \( sourceFileExtension ) to match jpg uti type " )
let baseFilename = sourceFilename . filenameWithoutExtension
dataSource . sourceFilename = baseFilename . appendingFileExtension ( " jpg " )
}
Logger . verbose ( " \( TAG ) Sending raw \( attachment . mimeType ) " )
return attachment
}
Logger . verbose ( " \( TAG ) Compressing attachment as image/jpeg, \( dataSource . dataLength ( ) ) bytes " )
return compressImageAsJPEG ( image : image , attachment : attachment , filename : dataSource . sourceFilename , imageQuality : imageQuality )
}
}
// I f t h e p r o p o s e d a t t a c h m e n t a l r e a d y c o n f o r m s t o t h e
// f i l e s i z e a n d c o n t e n t s i z e l i m i t s , d o n ' t r e c o m p r e s s i t .
private class func isValidOutputImage ( image : UIImage ? , dataSource : DataSource ? , dataUTI : String , imageQuality : TSImageQuality ) -> Bool {
guard let image = image else {
return false
}
guard let dataSource = dataSource else {
return false
}
guard SignalAttachment . outputImageUTISet . contains ( dataUTI ) else {
return false
}
if doesImageHaveAcceptableFileSize ( dataSource : dataSource , imageQuality : imageQuality ) &&
dataSource . dataLength ( ) <= kMaxFileSizeImage {
return true
}
return false
}
// F a c t o r y m e t h o d f o r a n i m a g e a t t a c h m e n t .
//
// N O T E : T h e a t t a c h m e n t r e t u r n e d b y t h i s m e t h o d m a y n i l o r n o t b e v a l i d .
// C h e c k t h e a t t a c h m e n t ' s e r r o r p r o p e r t y .
@objc
public class func imageAttachment ( image : UIImage ? , dataUTI : String , filename : String ? , imageQuality : TSImageQuality ) -> SignalAttachment {
assert ( dataUTI . count > 0 )
guard let image = image else {
let dataSource = DataSourceValue . emptyDataSource ( )
dataSource . sourceFilename = filename
let attachment = SignalAttachment ( dataSource : dataSource , dataUTI : dataUTI )
attachment . error = . missingData
return attachment
}
// M a k e a p l a c e h o l d e r a t t a c h m e n t o n w h i c h t o h a n g e r r o r s i f n e c e s s a r y .
let dataSource = DataSourceValue . emptyDataSource ( )
dataSource . sourceFilename = filename
let attachment = SignalAttachment ( dataSource : dataSource , dataUTI : dataUTI )
attachment . cachedImage = image
Logger . verbose ( " \( TAG ) Writing \( attachment . mimeType ) as image/jpeg " )
return compressImageAsJPEG ( image : image , attachment : attachment , filename : filename , imageQuality : imageQuality )
}
private class func compressImageAsJPEG ( image : UIImage , attachment : SignalAttachment , filename : String ? , imageQuality : TSImageQuality ) -> SignalAttachment {
assert ( attachment . error = = nil )
if imageQuality = = . original &&
attachment . dataLength < kMaxFileSizeGeneric {
// W e s h o u l d a v o i d r e s i z i n g i m a g e s a t t a c h e d " a s d o c u m e n t s " i f p o s s i b l e .
return attachment
}
var imageUploadQuality = imageQuality . imageQualityTier ( )
while true {
let maxSize = maxSizeForImage ( image : image , imageUploadQuality : imageUploadQuality )
var dstImage : UIImage ! = image
if image . size . width > maxSize ||
image . size . height > maxSize {
dstImage = imageScaled ( image , toMaxSize : maxSize )
}
guard let jpgImageData = UIImageJPEGRepresentation ( dstImage ,
jpegCompressionQuality ( imageUploadQuality : imageUploadQuality ) ) else {
attachment . error = . couldNotConvertToJpeg
return attachment
}
guard let dataSource = DataSourceValue . dataSource ( with : jpgImageData , fileExtension : " jpg " ) else {
attachment . error = . couldNotConvertToJpeg
return attachment
}
let baseFilename = filename ? . filenameWithoutExtension
let jpgFilename = baseFilename ? . appendingFileExtension ( " jpg " )
dataSource . sourceFilename = jpgFilename
if doesImageHaveAcceptableFileSize ( dataSource : dataSource , imageQuality : imageQuality ) &&
dataSource . dataLength ( ) <= kMaxFileSizeImage {
let recompressedAttachment = SignalAttachment ( dataSource : dataSource , dataUTI : kUTTypeJPEG as String )
recompressedAttachment . cachedImage = dstImage
Logger . verbose ( " \( TAG ) Converted \( attachment . mimeType ) to image/jpeg, \( jpgImageData . count ) bytes " )
return recompressedAttachment
}
// I f t h e J P E G o u t p u t i s l a r g e r t h a n t h e f i l e s i z e l i m i t ,
// c o n t i n u e t o t r y a g a i n b y p r o g r e s s i v e l y r e d u c i n g t h e
// i m a g e u p l o a d q u a l i t y .
switch imageUploadQuality {
case . original :
imageUploadQuality = . high
case . high :
imageUploadQuality = . mediumHigh
case . mediumHigh :
imageUploadQuality = . medium
case . medium :
imageUploadQuality = . mediumLow
case . mediumLow :
imageUploadQuality = . low
case . low :
attachment . error = . fileSizeTooLarge
return attachment
}
}
}
private class func imageScaled ( _ image : UIImage , toMaxSize size : CGFloat ) -> UIImage {
var scaleFactor : CGFloat
let aspectRatio : CGFloat = image . size . height / image . size . width
if aspectRatio > 1 {
scaleFactor = size / image . size . width
} else {
scaleFactor = size / image . size . height
}
let newSize = CGSize ( width : CGFloat ( image . size . width * scaleFactor ) , height : CGFloat ( image . size . height * scaleFactor ) )
UIGraphicsBeginImageContext ( newSize )
image . draw ( in : CGRect ( x : CGFloat ( 0 ) , y : CGFloat ( 0 ) , width : CGFloat ( newSize . width ) , height : CGFloat ( newSize . height ) ) )
let updatedImage : UIImage ? = UIGraphicsGetImageFromCurrentImageContext ( )
UIGraphicsEndImageContext ( )
return updatedImage !
}
private class func doesImageHaveAcceptableFileSize ( dataSource : DataSource , imageQuality : TSImageQuality ) -> Bool {
switch imageQuality {
case . original :
return true
case . medium :
return dataSource . dataLength ( ) < UInt ( 1024 * 1024 )
case . compact :
return dataSource . dataLength ( ) < UInt ( 400 * 1024 )
}
}
private class func maxSizeForImage ( image : UIImage , imageUploadQuality : TSImageQualityTier ) -> CGFloat {
switch imageUploadQuality {
case . original :
return max ( image . size . width , image . size . height )
case . high :
return 2048
case . mediumHigh :
return 1536
case . medium :
return 1024
case . mediumLow :
return 768
case . low :
return 512
}
}
private class func jpegCompressionQuality ( imageUploadQuality : TSImageQualityTier ) -> CGFloat {
switch imageUploadQuality {
case . original :
return 1
case . high :
return 0.9
case . mediumHigh :
return 0.8
case . medium :
return 0.7
case . mediumLow :
return 0.6
case . low :
return 0.5
}
}
// MARK: V i d e o A t t a c h m e n t s
// F a c t o r y m e t h o d f o r v i d e o a t t a c h m e n t s .
//
// N O T E : T h e a t t a c h m e n t r e t u r n e d b y t h i s m e t h o d m a y n o t b e v a l i d .
// C h e c k t h e a t t a c h m e n t ' s e r r o r p r o p e r t y .
private class func videoAttachment ( dataSource : DataSource ? , dataUTI : String ) -> SignalAttachment {
guard let dataSource = dataSource else {
let dataSource = DataSourceValue . emptyDataSource ( )
let attachment = SignalAttachment ( dataSource : dataSource , dataUTI : dataUTI )
attachment . error = . missingData
return attachment
}
if ! isValidOutputVideo ( dataSource : dataSource , dataUTI : dataUTI ) {
owsFail ( " building video with invalid output, migrate to async API using compressVideoAsMp4 " )
}
return newAttachment ( dataSource : dataSource ,
dataUTI : dataUTI ,
validUTISet : videoUTISet ,
maxFileSize : kMaxFileSizeVideo )
}
public class func copyToVideoTempDir ( url fromUrl : URL ) throws -> URL {
let baseDir = SignalAttachment . videoTempPath . appendingPathComponent ( UUID ( ) . uuidString , isDirectory : true )
OWSFileSystem . ensureDirectoryExists ( baseDir . path )
let toUrl = baseDir . appendingPathComponent ( fromUrl . lastPathComponent )
Logger . debug ( " \( self . logTag ) moving \( fromUrl ) -> \( toUrl ) " )
try FileManager . default . copyItem ( at : fromUrl , to : toUrl )
return toUrl
}
private class var videoTempPath : URL {
let videoDir = URL ( fileURLWithPath : NSTemporaryDirectory ( ) ) . appendingPathComponent ( " video " )
OWSFileSystem . ensureDirectoryExists ( videoDir . path )
return videoDir
}
public class func compressVideoAsMp4 ( dataSource : DataSource , dataUTI : String ) -> ( Promise < SignalAttachment > , AVAssetExportSession ? ) {
Logger . debug ( " \( self . TAG ) in \( #function ) " )
guard let url = dataSource . dataUrl ( ) else {
let attachment = SignalAttachment ( dataSource : DataSourceValue . emptyDataSource ( ) , dataUTI : dataUTI )
attachment . error = . missingData
return ( Promise ( value : attachment ) , nil )
}
let asset = AVAsset ( url : url )
guard let exportSession = AVAssetExportSession ( asset : asset , presetName : AVAssetExportPresetMediumQuality ) else {
let attachment = SignalAttachment ( dataSource : DataSourceValue . emptyDataSource ( ) , dataUTI : dataUTI )
attachment . error = . couldNotConvertToMpeg4
return ( Promise ( value : attachment ) , nil )
}
exportSession . shouldOptimizeForNetworkUse = true
exportSession . outputFileType = AVFileTypeMPEG4
let exportURL = videoTempPath . appendingPathComponent ( UUID ( ) . uuidString ) . appendingPathExtension ( " mp4 " )
exportSession . outputURL = exportURL
let ( promise , fulfill , _ ) = Promise < SignalAttachment > . pending ( )
Logger . debug ( " \( self . TAG ) starting video export " )
exportSession . exportAsynchronously {
Logger . debug ( " \( self . TAG ) Completed video export " )
let baseFilename = dataSource . sourceFilename
let mp4Filename = baseFilename ? . filenameWithoutExtension . appendingFileExtension ( " mp4 " )
guard let dataSource = DataSourcePath . dataSource ( with : exportURL ) else {
owsFail ( " Failed to build data source for exported video URL " )
let attachment = SignalAttachment ( dataSource : DataSourceValue . emptyDataSource ( ) , dataUTI : dataUTI )
attachment . error = . couldNotConvertToMpeg4
fulfill ( attachment )
return
}
dataSource . setShouldDeleteOnDeallocation ( )
dataSource . sourceFilename = mp4Filename
let attachment = SignalAttachment ( dataSource : dataSource , dataUTI : kUTTypeMPEG4 as String )
fulfill ( attachment )
}
return ( promise , exportSession )
}
@objc
public class VideoCompressionResult : NSObject {
@objc
public let attachmentPromise : AnyPromise
@objc
public let exportSession : AVAssetExportSession ?
fileprivate init ( attachmentPromise : Promise < SignalAttachment > , exportSession : AVAssetExportSession ? ) {
self . attachmentPromise = AnyPromise ( attachmentPromise )
self . exportSession = exportSession
super . init ( )
}
}
@objc
public class func compressVideoAsMp4 ( dataSource : DataSource , dataUTI : String ) -> VideoCompressionResult {
let ( attachmentPromise , exportSession ) = compressVideoAsMp4 ( dataSource : dataSource , dataUTI : dataUTI )
return VideoCompressionResult ( attachmentPromise : attachmentPromise , exportSession : exportSession )
}
public class func isInvalidVideo ( dataSource : DataSource , dataUTI : String ) -> Bool {
guard videoUTISet . contains ( dataUTI ) else {
// n o t a v i d e o
return false
}
guard isValidOutputVideo ( dataSource : dataSource , dataUTI : dataUTI ) else {
// f o u n d a v i d e o w h i c h n e e d s t o b e c o n v e r t e d
return true
}
// I t i s a v i d e o , b u t i t ' s n o t i n v a l i d
return false
}
private class func isValidOutputVideo ( dataSource : DataSource ? , dataUTI : String ) -> Bool {
guard let dataSource = dataSource else {
return false
}
guard SignalAttachment . outputVideoUTISet . contains ( dataUTI ) else {
return false
}
if dataSource . dataLength ( ) <= kMaxFileSizeVideo {
return true
}
return false
}
// MARK: A u d i o A t t a c h m e n t s
// F a c t o r y m e t h o d f o r a u d i o a t t a c h m e n t s .
//
// N O T E : T h e a t t a c h m e n t r e t u r n e d b y t h i s m e t h o d m a y n o t b e v a l i d .
// C h e c k t h e a t t a c h m e n t ' s e r r o r p r o p e r t y .
private class func audioAttachment ( dataSource : DataSource ? , dataUTI : String ) -> SignalAttachment {
return newAttachment ( dataSource : dataSource ,
dataUTI : dataUTI ,
validUTISet : audioUTISet ,
maxFileSize : kMaxFileSizeAudio )
}
// MARK: O v e r s i z e T e x t A t t a c h m e n t s
// F a c t o r y m e t h o d f o r o v e r s i z e t e x t a t t a c h m e n t s .
//
// N O T E : T h e a t t a c h m e n t r e t u r n e d b y t h i s m e t h o d m a y n o t b e v a l i d .
// C h e c k t h e a t t a c h m e n t ' s e r r o r p r o p e r t y .
private class func oversizeTextAttachment ( text : String ? ) -> SignalAttachment {
let dataSource = DataSourceValue . dataSource ( withOversizeText : text )
return newAttachment ( dataSource : dataSource ,
dataUTI : kOversizeTextAttachmentUTI ,
validUTISet : nil ,
maxFileSize : kMaxFileSizeGeneric )
}
// MARK: G e n e r i c A t t a c h m e n t s
// F a c t o r y m e t h o d f o r g e n e r i c a t t a c h m e n t s .
//
// N O T E : T h e a t t a c h m e n t r e t u r n e d b y t h i s m e t h o d m a y n o t b e v a l i d .
// C h e c k t h e a t t a c h m e n t ' s e r r o r p r o p e r t y .
private class func genericAttachment ( dataSource : DataSource ? , dataUTI : String ) -> SignalAttachment {
return newAttachment ( dataSource : dataSource ,
dataUTI : dataUTI ,
validUTISet : nil ,
maxFileSize : kMaxFileSizeGeneric )
}
// MARK: V o i c e M e s s a g e s
@objc
public class func voiceMessageAttachment ( dataSource : DataSource ? , dataUTI : String ) -> SignalAttachment {
let attachment = audioAttachment ( dataSource : dataSource , dataUTI : dataUTI )
attachment . isVoiceMessage = true
return attachment
}
// MARK: A t t a c h m e n t s
// F a c t o r y m e t h o d f o r a t t a c h m e n t s o f a n y k i n d .
//
// N O T E : T h e a t t a c h m e n t r e t u r n e d b y t h i s m e t h o d m a y n o t b e v a l i d .
// C h e c k t h e a t t a c h m e n t ' s e r r o r p r o p e r t y .
@objc
public class func attachment ( dataSource : DataSource ? , dataUTI : String ) -> SignalAttachment {
if inputImageUTISet . contains ( dataUTI ) {
owsFail ( " \( TAG ) must specify image quality type " )
}
return attachment ( dataSource : dataSource , dataUTI : dataUTI , imageQuality : . original )
}
// F a c t o r y m e t h o d f o r a t t a c h m e n t s o f a n y k i n d .
//
// N O T E : T h e a t t a c h m e n t r e t u r n e d b y t h i s m e t h o d m a y n o t b e v a l i d .
// C h e c k t h e a t t a c h m e n t ' s e r r o r p r o p e r t y .
@objc
public class func attachment ( dataSource : DataSource ? , dataUTI : String , imageQuality : TSImageQuality ) -> SignalAttachment {
if inputImageUTISet . contains ( dataUTI ) {
return imageAttachment ( dataSource : dataSource , dataUTI : dataUTI , imageQuality : imageQuality )
} else if videoUTISet . contains ( dataUTI ) {
return videoAttachment ( dataSource : dataSource , dataUTI : dataUTI )
} else if audioUTISet . contains ( dataUTI ) {
return audioAttachment ( dataSource : dataSource , dataUTI : dataUTI )
} else {
return genericAttachment ( dataSource : dataSource , dataUTI : dataUTI )
}
}
@objc
public class func empty ( ) -> SignalAttachment {
return SignalAttachment . attachment ( dataSource : DataSourceValue . emptyDataSource ( ) ,
dataUTI : kUTTypeContent as String ,
imageQuality : . original )
}
// MARK: H e l p e r M e t h o d s
private class func newAttachment ( dataSource : DataSource ? ,
dataUTI : String ,
validUTISet : Set < String > ? ,
maxFileSize : UInt ) -> SignalAttachment {
assert ( dataUTI . count > 0 )
assert ( dataSource != nil )
guard let dataSource = dataSource else {
let attachment = SignalAttachment ( dataSource : DataSourceValue . emptyDataSource ( ) , dataUTI : dataUTI )
attachment . error = . missingData
return attachment
}
let attachment = SignalAttachment ( dataSource : dataSource , dataUTI : dataUTI )
if let validUTISet = validUTISet {
guard validUTISet . contains ( dataUTI ) else {
attachment . error = . invalidFileFormat
return attachment
}
}
guard dataSource . dataLength ( ) > 0 else {
owsFail ( " \( TAG ) Empty attachment " )
assert ( dataSource . dataLength ( ) > 0 )
attachment . error = . invalidData
return attachment
}
guard dataSource . dataLength ( ) <= maxFileSize else {
attachment . error = . fileSizeTooLarge
return attachment
}
// A t t a c h m e n t i s v a l i d
return attachment
}
}