Merge branch 'charlesmchen/imageQualityRevisited'

pull/1/head
Matthew Chen 8 years ago
commit c1d435c9d8

@ -2481,7 +2481,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
}
[dataSource setSourceFilename:filename];
SignalAttachment *attachment = [SignalAttachment attachmentWithDataSource:dataSource dataUTI:type];
// "Document picker" attachments _SHOULD NOT_ be resized, if possible.
SignalAttachment *attachment =
[SignalAttachment attachmentWithDataSource:dataSource dataUTI:type imageQuality:TSImageQualityOriginal];
[self tryToSendAttachmentIfApproved:attachment];
}
@ -2490,7 +2492,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
/*
* Presenting UIImagePickerController
*/
- (void)takePictureOrVideo
{
[self ows_askForCameraPermissions:^(BOOL granted) {
@ -2605,10 +2606,12 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
OWSAssert([NSThread isMainThread]);
if (imageFromCamera) {
// "Camera" attachments _SHOULD_ be resized, if possible.
SignalAttachment *attachment =
[SignalAttachment imageAttachmentWithImage:imageFromCamera
dataUTI:(NSString *)kUTTypeJPEG
filename:filename];
filename:filename
imageQuality:TSImageQualityCompact];
if (!attachment || [attachment hasError]) {
DDLogWarn(@"%@ %s Invalid attachment: %@.",
self.logTag,
@ -2655,8 +2658,11 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
DataSource *_Nullable dataSource =
[DataSourceValue dataSourceWithData:imageData utiType:dataUTI];
[dataSource setSourceFilename:filename];
// "Camera Roll" attachments _SHOULD_ be resized, if possible.
SignalAttachment *attachment =
[SignalAttachment attachmentWithDataSource:dataSource dataUTI:dataUTI];
[SignalAttachment attachmentWithDataSource:dataSource
dataUTI:dataUTI
imageQuality:TSImageQualityMedium];
[self dismissViewControllerAnimated:YES
completion:^{
OWSAssert([NSThread isMainThread]);

@ -372,7 +372,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
owsFail("\(strongSelf.TAG) couldn't load asset.")
return
}
let attachment = SignalAttachment.imageAttachment(dataSource: dataSource, dataUTI: asset.rendition.utiType)
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: asset.rendition.utiType)
strongSelf.delegate?.gifPickerDidSelect(attachment: attachment)

@ -53,13 +53,34 @@ extension SignalAttachmentError: LocalizedError {
}
}
enum TSImageQuality {
case uncropped
@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
}
}
}
// Represents a possible attachment to upload.
// The attachment may be invalid.
//
@ -169,7 +190,7 @@ public class SignalAttachment: NSObject {
public var errorName: String? {
guard let error = error else {
// This method should only be called if there is an error.
owsFail("Missing error")
owsFail("\(TAG) Missing error")
return nil
}
@ -180,7 +201,7 @@ public class SignalAttachment: NSObject {
public var localizedErrorDescription: String? {
guard let error = self.error else {
// This method should only be called if there is an error.
owsFail("Missing error")
owsFail("\(TAG) Missing error")
return nil
}
@ -448,7 +469,8 @@ public class SignalAttachment: NSObject {
return nil
}
let dataSource = DataSourceValue.dataSource(with:data, utiType: dataUTI)
return imageAttachment(dataSource : dataSource, dataUTI : dataUTI)
// Pasted images _SHOULD _NOT_ be resized, if possible.
return attachment(dataSource : dataSource, dataUTI : dataUTI, imageQuality:.medium)
}
}
for dataUTI in videoUTISet {
@ -507,7 +529,7 @@ public class SignalAttachment: NSObject {
// NOTE: The attachment returned by this method may not be valid.
// Check the attachment's error property.
@objc
public class func imageAttachment(dataSource: DataSource?, dataUTI: String) -> SignalAttachment {
private class func imageAttachment(dataSource: DataSource?, dataUTI: String, imageQuality: TSImageQuality) -> SignalAttachment {
assert(dataUTI.count > 0)
assert(dataSource != nil)
@ -546,7 +568,7 @@ public class SignalAttachment: NSObject {
}
attachment.cachedImage = image
if isInputImageValidOutputImage(image: image, dataSource: dataSource, dataUTI: dataUTI) {
if isInputImageValidOutputImage(image: image, dataSource: dataSource, dataUTI: dataUTI, imageQuality:imageQuality) {
if let sourceFilename = dataSource.sourceFilename,
let sourceFileExtension = sourceFilename.fileExtension,
["heic", "heif"].contains(sourceFileExtension.lowercased()) {
@ -570,19 +592,14 @@ public class SignalAttachment: NSObject {
return attachment
}
Logger.verbose("\(TAG) Compressing attachment as image/jpeg")
return compressImageAsJPEG(image : image, attachment : attachment, filename:dataSource.sourceFilename)
Logger.verbose("\(TAG) Compressing attachment as image/jpeg, \(dataSource.dataLength()) bytes")
return compressImageAsJPEG(image : image, attachment : attachment, filename:dataSource.sourceFilename, imageQuality:imageQuality)
}
}
private class func defaultImageUploadQuality() -> TSImageQuality {
// Currently default to a original image quality and size.
return .uncropped
}
// If the proposed attachment already conforms to the
// file size and content size limits, don't recompress it.
private class func isInputImageValidOutputImage(image: UIImage?, dataSource: DataSource?, dataUTI: String) -> Bool {
private class func isInputImageValidOutputImage(image: UIImage?, dataSource: DataSource?, dataUTI: String, imageQuality: TSImageQuality) -> Bool {
guard let image = image else {
return false
}
@ -593,10 +610,7 @@ public class SignalAttachment: NSObject {
return false
}
let maxSize = maxSizeForImage(image: image,
imageUploadQuality:defaultImageUploadQuality())
if image.size.width <= maxSize &&
image.size.height <= maxSize &&
if doesImageHaveAcceptableFileSize(dataSource: dataSource, imageQuality: imageQuality) &&
dataSource.dataLength() <= kMaxFileSizeImage {
return true
}
@ -608,7 +622,7 @@ public class SignalAttachment: NSObject {
// NOTE: The attachment returned by this method may nil or not be valid.
// Check the attachment's error property.
@objc
public class func imageAttachment(image: UIImage?, dataUTI: String, filename: String?) -> SignalAttachment {
public class func imageAttachment(image: UIImage?, dataUTI: String, filename: String?, imageQuality: TSImageQuality) -> SignalAttachment {
assert(dataUTI.count > 0)
guard let image = image else {
@ -626,13 +640,13 @@ public class SignalAttachment: NSObject {
attachment.cachedImage = image
Logger.verbose("\(TAG) Writing \(attachment.mimeType) as image/jpeg")
return compressImageAsJPEG(image : image, attachment : attachment, filename:filename)
return compressImageAsJPEG(image : image, attachment : attachment, filename:filename, imageQuality:imageQuality)
}
private class func compressImageAsJPEG(image: UIImage, attachment: SignalAttachment, filename: String?) -> SignalAttachment {
private class func compressImageAsJPEG(image: UIImage, attachment: SignalAttachment, filename: String?, imageQuality: TSImageQuality) -> SignalAttachment {
assert(attachment.error == nil)
var imageUploadQuality = defaultImageUploadQuality()
var imageUploadQuality = imageQuality.imageQualityTier()
while true {
let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
@ -656,9 +670,11 @@ public class SignalAttachment: NSObject {
let jpgFilename = baseFilename?.appendingFileExtension("jpg")
dataSource.sourceFilename = jpgFilename
if UInt(jpgImageData.count) <= kMaxFileSizeImage {
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
}
@ -666,11 +682,15 @@ public class SignalAttachment: NSObject {
// continue to try again by progressively reducing the
// image upload quality.
switch imageUploadQuality {
case .uncropped:
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
@ -695,29 +715,48 @@ public class SignalAttachment: NSObject {
return updatedImage!
}
private class func maxSizeForImage(image: UIImage, imageUploadQuality: TSImageQuality) -> CGFloat {
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 .uncropped:
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: TSImageQuality) -> CGFloat {
private class func jpegCompressionQuality(imageUploadQuality: TSImageQualityTier) -> CGFloat {
switch imageUploadQuality {
case .uncropped:
case .original:
return 1
case .high:
return 0.9
case .mediumHigh:
return 0.8
case .medium:
return 0.5
return 0.7
case .mediumLow:
return 0.6
case .low:
return 0.3
return 0.5
}
}
@ -792,7 +831,19 @@ public class SignalAttachment: NSObject {
@objc
public class func attachment(dataSource: DataSource?, dataUTI: String) -> SignalAttachment {
if inputImageUTISet.contains(dataUTI) {
return imageAttachment(dataSource : dataSource, dataUTI : dataUTI)
owsFail("\(TAG) must specify image quality type")
}
return attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .original)
}
// Factory method for attachments of any kind.
//
// NOTE: The attachment returned by this method may not be valid.
// Check the attachment's error property.
@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) {
@ -805,7 +856,8 @@ public class SignalAttachment: NSObject {
@objc
public class func empty() -> SignalAttachment {
return SignalAttachment.attachment(dataSource : DataSourceValue.emptyDataSource(),
dataUTI: kUTTypeContent as String)
dataUTI: kUTTypeContent as String,
imageQuality:.original)
}
// MARK: Helper Methods
@ -833,7 +885,7 @@ public class SignalAttachment: NSObject {
}
guard dataSource.dataLength() > 0 else {
owsFail("Empty attachment")
owsFail("\(TAG) Empty attachment")
assert(dataSource.dataLength() > 0)
attachment.error = .invalidData
return attachment

@ -435,7 +435,7 @@ public class ShareViewController: UINavigationController, ShareViewDelegate, SAE
}
}
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType)
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality:.medium)
return attachment
}

Loading…
Cancel
Save