diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 65c0f8631..be57dca33 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -148,7 +148,6 @@ 3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; }; 347850551FD749C0007B8332 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; }; 347850571FD86544007B8332 /* SAEFailedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850561FD86544007B8332 /* SAEFailedViewController.swift */; }; - 347850591FD9972E007B8332 /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850581FD9972E007B8332 /* SwiftSingletons.swift */; }; 347850691FD9B78A007B8332 /* AppSetup.m in Sources */ = {isa = PBXBuildFile; fileRef = 347850651FD9B789007B8332 /* AppSetup.m */; }; 3478506A1FD9B78A007B8332 /* AppSetup.h in Headers */ = {isa = PBXBuildFile; fileRef = 347850661FD9B789007B8332 /* AppSetup.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3478506B1FD9B78A007B8332 /* NoopCallMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850671FD9B78A007B8332 /* NoopCallMessageHandler.swift */; }; @@ -783,7 +782,6 @@ 34661FB720C1C0D60056EDD6 /* message_sent.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; name = message_sent.aiff; path = Signal/AudioFiles/message_sent.aiff; sourceTree = SOURCE_ROOT; }; 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = ""; }; 347850561FD86544007B8332 /* SAEFailedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEFailedViewController.swift; sourceTree = ""; }; - 347850581FD9972E007B8332 /* SwiftSingletons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSingletons.swift; sourceTree = ""; }; 3478505A1FD999D5007B8332 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = translations/et.lproj/Localizable.strings; sourceTree = ""; }; 3478505C1FD99A1F007B8332 /* zh_TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_TW; path = translations/zh_TW.lproj/Localizable.strings; sourceTree = ""; }; 347850651FD9B789007B8332 /* AppSetup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppSetup.m; sourceTree = ""; }; @@ -1559,7 +1557,6 @@ 34641E1120878FB000E2EDE5 /* OWSWindowManager.h */, 34641E1020878FAF00E2EDE5 /* OWSWindowManager.m */, 45360B8C1F9521F800FA666C /* Searcher.swift */, - 347850581FD9972E007B8332 /* SwiftSingletons.swift */, 346129BD1FD2068600532771 /* ThreadUtil.h */, 346129BE1FD2068600532771 /* ThreadUtil.m */, B97940251832BD2400BD66CB /* UIUtil.h */, @@ -3208,7 +3205,6 @@ 346129AB1FD1F0EE00532771 /* OWSFormat.m in Sources */, 34AC0A12211B39EA00997B47 /* ContactTableViewCell.m in Sources */, 451F8A461FD715BA005CB9DA /* OWSGroupAvatarBuilder.m in Sources */, - 347850591FD9972E007B8332 /* SwiftSingletons.swift in Sources */, 346129961FD1E30000532771 /* OWSDatabaseMigration.m in Sources */, 346129FB1FD5F31400532771 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */, 34AC09EA211B39B100997B47 /* ModalActivityIndicatorViewController.swift in Sources */, diff --git a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift new file mode 100644 index 000000000..f18f7af99 --- /dev/null +++ b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift @@ -0,0 +1,194 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +private struct OWSThumbnailRequest { + public typealias CompletionBlock = (UIImage) -> Void + + let attachmentId: String + let thumbnailDimensionPoints: UInt + let completion: CompletionBlock + + init(attachmentId: String, thumbnailDimensionPoints: UInt, completion: @escaping CompletionBlock) { + self.attachmentId = attachmentId + self.thumbnailDimensionPoints = thumbnailDimensionPoints + self.completion = completion + } +} + +@objc public class OWSThumbnailService: NSObject { + + // MARK: - Singleton class + + @objc(shared) + public static let shared = OWSThumbnailService() + + public typealias CompletionBlock = (UIImage) -> Void + + private let serialQueue = DispatchQueue(label: "OWSThumbnailService") + + private let dbConnection: YapDatabaseConnection + + // This property should only be accessed on the serialQueue. + // + // We want to process requests in _reverse_ order in which they + // arrive so that we prioritize the most recent view state. + // This data structure is actually used like a stack. + private var thumbnailRequestQueue = [OWSThumbnailRequest]() + + private override init() { + + dbConnection = OWSPrimaryStorage.shared().newDatabaseConnection() + + super.init() + + SwiftSingletons.register(self) + } + + private func canThumbnailAttachment(attachment: TSAttachmentStream) -> Bool { + guard attachment.isImage() else { + return false + } + guard !attachment.isAnimated() else { + return false + } + guard attachment.isValidImage() else { + return false + } + return true + } + + // completion will only be called on success. + // completion will be called async on the main thread. + @objc public func ensureThumbnailForAttachmentId(attachmentId: String, + thumbnailDimensionPoints: UInt, + completion:@escaping CompletionBlock) { + guard attachmentId.count > 0 else { + owsFail("Empty attachment id.") + return + } + serialQueue.async { + let thumbnailRequest = OWSThumbnailRequest(attachmentId: attachmentId, thumbnailDimensionPoints: thumbnailDimensionPoints, completion: completion) + self.thumbnailRequestQueue.append(thumbnailRequest) + + self.processNextRequestSync() + } + } + + private func processNextRequestAsync() { + serialQueue.async { + self.processNextRequestSync() + } + } + + // This should only be called on the serialQueue. + private func processNextRequestSync() { + guard !thumbnailRequestQueue.isEmpty else { + return + } + let thumbnailRequest = thumbnailRequestQueue.removeLast() + + if let image = process(thumbnailRequest: thumbnailRequest) { + DispatchQueue.main.async { + thumbnailRequest.completion(image) + } + } + } + + // This should only be called on the serialQueue. + private func process(thumbnailRequest: OWSThumbnailRequest) -> UIImage? { + var possibleAttachment: TSAttachmentStream? + self.dbConnection.read({ (transaction) in + possibleAttachment = TSAttachmentStream.fetch(uniqueId: thumbnailRequest.attachmentId, transaction: transaction) + }) + guard let attachment = possibleAttachment else { + Logger.warn("Could not load attachment for thumbnailing.") + return nil + } + guard canThumbnailAttachment(attachment: attachment) else { + Logger.warn("Cannot thumbnail attachment.") + return nil + } + if let thumbnails = attachment.thumbnails { + for thumbnail in thumbnails { + if thumbnail.thumbnailDimensionPoints == thumbnailRequest.thumbnailDimensionPoints { + guard let filePath = attachment.path(for: thumbnail) else { + owsFail("Could not determine thumbnail path.") + return nil + } + guard let image = UIImage(contentsOfFile: filePath) else { + owsFail("Could not load thumbnail.") + return nil + } + return image + } + } + } + guard let originalFilePath = attachment.originalFilePath() else { + owsFail("Could not determine thumbnail path.") + return nil + } + guard let originalImage = UIImage(contentsOfFile: originalFilePath) else { + owsFail("Could not load original image.") + return nil + } + let originalSize = originalImage.size + guard originalSize.width > 0 && originalSize.height > 0 else { + owsFail("Original image has invalid size.") + return nil + } + var thumbnailSize = CGSize.zero + if originalSize.width > originalSize.height { + thumbnailSize.width = CGFloat(thumbnailRequest.thumbnailDimensionPoints) + thumbnailSize.height = round(CGFloat(thumbnailRequest.thumbnailDimensionPoints) * thumbnailSize.height / thumbnailSize.width) + } else { + thumbnailSize.width = round(CGFloat(thumbnailRequest.thumbnailDimensionPoints) * thumbnailSize.width / thumbnailSize.height) + thumbnailSize.height = CGFloat(thumbnailRequest.thumbnailDimensionPoints) + } + guard thumbnailSize.width > 0 && thumbnailSize.height > 0 else { + owsFail("Thumbnail has invalid size.") + return nil + } + guard originalSize.width < thumbnailSize.width && + originalSize.height < thumbnailSize.height else { + owsFail("Thumbnail isn't smaller than the original.") + return nil + } + // We use UIGraphicsBeginImageContextWithOptions() to scale. + // Core Image would provide better quality (e.g. Lanczos) but + // at perf cost we don't want to pay. We could also use + // CoreGraphics directly, but I'm not sure there's any benefit. + guard let thumbnailImage = originalImage.resizedImage(to: thumbnailSize) else { + owsFail("Could not thumbnail image.") + return nil + } + guard let thumbnailData = UIImageJPEGRepresentation(thumbnailImage, 0.85) else { + owsFail("Could not convert thumbnail to JPEG.") + return nil + } + let temporaryDirectory = NSTemporaryDirectory() + let thumbnailFilename = "\(NSUUID().uuidString).jpg" + let thumbnailFilePath = (temporaryDirectory as NSString).appendingPathComponent(thumbnailFilename) + do { + try thumbnailData.write(to: NSURL.fileURL(withPath: thumbnailFilePath), options: .atomicWrite) + } catch let error as NSError { + owsFail("File write failed: \(thumbnailFilePath), \(error)") + return nil + } + // It should be safe to assume that an attachment will never end up with two thumbnails of + // the same size since: + // + // * Thumbnails are only added by this method. + // * This method checks for an existing thumbnail using the same connection. + // * This method is performed on the serial queue. + self.dbConnection.readWrite({ (transaction) in + attachment.update(withNewThumbnail: thumbnailFilePath, + thumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints, + size: thumbnailSize, + transaction: transaction) + }) + return thumbnailImage + } +} diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h index 3483184b5..649d9cfc4 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h @@ -17,6 +17,21 @@ NS_ASSUME_NONNULL_BEGIN @class TSAttachmentPointer; @class YapDatabaseReadWriteTransaction; +typedef void (^OWSThumbnailCompletion)(UIImage *image); + +@interface TSAttachmentThumbnail : MTLModel + +@property (nonatomic, readonly) NSString *filename; +@property (nonatomic, readonly) CGSize size; +// The length of the longer side. +@property (nonatomic, readonly) NSUInteger thumbnailDimensionPoints; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +#pragma mark - + @interface TSAttachmentStream : TSAttachment - (instancetype)init NS_UNAVAILABLE; @@ -36,6 +51,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSDate *creationTimestamp; +@property (nonatomic, nullable, readonly) NSArray *thumbnails; + #if TARGET_OS_IPHONE - (nullable NSData *)validStillImageData; #endif @@ -49,6 +66,7 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSString *)originalFilePath; - (nullable NSURL *)originalMediaURL; +// TODO: Rename to legacy... - (nullable UIImage *)thumbnailImage; - (nullable NSData *)thumbnailData; - (nullable NSString *)thumbnailPath; @@ -78,6 +96,20 @@ NS_ASSUME_NONNULL_BEGIN // Non-nil for attachments which need "lazy backup restore." - (nullable OWSBackupFragment *)lazyRestoreFragment; + +#pragma mark - Thumbnails + +// On cache hit, the thumbnail will be returned synchronously and completion will never be invoked. +// On cache miss, nil will be returned and the completion will be invoked async on main if +// thumbnail can be generated. +- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint completion:(OWSThumbnailCompletion)completion; +- (nullable UIImage *)thumbnailImageSmallWithCompletion:(OWSThumbnailCompletion)completion; +- (nullable UIImage *)thumbnailImageMediumWithCompletion:(OWSThumbnailCompletion)completion; +- (nullable UIImage *)thumbnailImageLargeWithCompletion:(OWSThumbnailCompletion)completion; + +// This method should only be invoked by OWSThumbnailService. +- (nullable NSString *)pathForThumbnail:(TSAttachmentThumbnail *)thumbnail; + #pragma mark - Validation - (BOOL)isValidImage; @@ -91,8 +123,14 @@ NS_ASSUME_NONNULL_BEGIN // Marks attachment as having completed "lazy backup restore." - (void)updateWithLazyRestoreComplete; +// TODO: Review. - (nullable TSAttachmentStream *)cloneAsThumbnail; +- (void)updateWithNewThumbnail:(NSString *)tempFilePath + thumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints + size:(CGSize)size + transaction:(YapDatabaseReadWriteTransaction *)transaction; + #pragma mark - Protobuf + (nullable SSKProtoAttachmentPointer *)buildProtoForAttachmentId:(nullable NSString *)attachmentId; diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index f2f019d05..8f449d780 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -16,6 +16,36 @@ NS_ASSUME_NONNULL_BEGIN const CGFloat kMaxVideoStillSize = 1 * 1024; +const NSUInteger kThumbnailDimensionPointsSmall = 200; +const NSUInteger kThumbnailDimensionPointsMedium = 800; +// This size is large enough to render full screen. +const NSUInteger ThumbnailDimensionPointsLarge() { + CGSize screenSizePoints = UIScreen.mainScreen.bounds.size; + return MAX(screenSizePoints.width, screenSizePoints.height); +} + +@implementation TSAttachmentThumbnail + +- (instancetype)initWithFilename:(NSString *)filename + size:(CGSize)size + thumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints +{ + self = [super init]; + if (!self) { + return self; + } + + _filename = filename; + _size = size; + _thumbnailDimensionPoints = thumbnailDimensionPoints; + + return self; +} + +@end + +#pragma mark - + @interface TSAttachmentStream () // We only want to generate the file path for this attachment once, so that @@ -32,6 +62,8 @@ const CGFloat kMaxVideoStillSize = 1 * 1024; // Optional property. Only set for attachments which need "lazy backup restore." @property (nonatomic, nullable) NSString *lazyRestoreFragmentId; +@property (nonatomic, nullable) NSArray *thumbnails; + @end #pragma mark - @@ -258,6 +290,18 @@ const CGFloat kMaxVideoStillSize = 1 * 1024; return [[containingDir stringByAppendingPathComponent:newFilename] stringByAppendingPathExtension:@"jpg"]; } +- (nullable NSString *)pathForThumbnail:(TSAttachmentThumbnail *)thumbnail +{ + NSString *filePath = self.originalFilePath; + if (!filePath) { + OWSFail(@"%@ Attachment missing local file path.", self.logTag); + return nil; + } + + NSString *containingDir = filePath.stringByDeletingLastPathComponent; + return [containingDir stringByAppendingPathComponent:thumbnail.filename]; +} + - (nullable NSURL *)originalMediaURL { NSString *_Nullable filePath = self.originalFilePath; @@ -272,12 +316,22 @@ const CGFloat kMaxVideoStillSize = 1 * 1024; { NSError *error; + for (TSAttachmentThumbnail *thumbnail in self.thumbnails) { + NSString *_Nullable thumbnailPath = [self pathForThumbnail:thumbnail]; + if (thumbnailPath) { + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:thumbnailPath error:&error]; + if (error || !success) { + DDLogError(@"%@ remove thumbnail failed with: %@", self.logTag, error); + } + } + } + NSString *_Nullable thumbnailPath = self.thumbnailPath; if (thumbnailPath) { - [[NSFileManager defaultManager] removeItemAtPath:thumbnailPath error:&error]; + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:thumbnailPath error:&error]; - if (error) { - DDLogError(@"%@ remove thumbnail errored with: %@", self.logTag, error); + if (error || !success) { + DDLogError(@"%@ remove legacy thumbnail failed with: %@", self.logTag, error); } } @@ -286,10 +340,9 @@ const CGFloat kMaxVideoStillSize = 1 * 1024; OWSFail(@"%@ Missing path for attachment.", self.logTag); return; } - [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; - - if (error) { - DDLogError(@"%@ remove file errored with: %@", self.logTag, error); + BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + if (error || !success) { + DDLogError(@"%@ remove file failed with: %@", self.logTag, error); } } @@ -728,6 +781,70 @@ const CGFloat kMaxVideoStillSize = 1 * 1024; return string; } +#pragma mark - Thumbnails + +- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint completion:(OWSThumbnailCompletion)completion +{ + CGFloat maxDimensionHint = MAX(sizeHint.width, sizeHint.height); + NSUInteger thumbnailDimensionPoints; + if (maxDimensionHint <= kThumbnailDimensionPointsSmall) { + thumbnailDimensionPoints = kThumbnailDimensionPointsSmall; + } else if (maxDimensionHint <= kThumbnailDimensionPointsMedium) { + thumbnailDimensionPoints = kThumbnailDimensionPointsMedium; + } else { + thumbnailDimensionPoints = ThumbnailDimensionPointsLarge(); + } + + return [self thumbnailImageWithThumbnailDimensionPoints:thumbnailDimensionPoints completion:completion]; +} + +- (nullable UIImage *)thumbnailImageSmallWithCompletion:(OWSThumbnailCompletion)completion +{ + return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall completion:completion]; +} + +- (nullable UIImage *)thumbnailImageMediumWithCompletion:(OWSThumbnailCompletion)completion +{ + return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsMedium completion:completion]; +} + +- (nullable UIImage *)thumbnailImageLargeWithCompletion:(OWSThumbnailCompletion)completion +{ + return [self thumbnailImageWithThumbnailDimensionPoints:ThumbnailDimensionPointsLarge() completion:completion]; +} + +- (nullable UIImage *)thumbnailImageWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints + completion:(OWSThumbnailCompletion)completion +{ + CGSize originalSize = self.imageSize; + if (originalSize.width < 1 || originalSize.height < 1) { + return nil; + } + if (originalSize.width <= thumbnailDimensionPoints || originalSize.height <= thumbnailDimensionPoints) { + // There's no point in generating a thumbnail if the original is smaller than the + // thumbnail size. + return self.originalImage; + } + + for (TSAttachmentThumbnail *thumbnail in self.thumbnails) { + if (thumbnail.thumbnailDimensionPoints != thumbnailDimensionPoints) { + continue; + } + NSString *_Nullable thumbnailPath = [self pathForThumbnail:thumbnail]; + if (!thumbnailPath) { + OWSFail(@"Missing thumbnail path."); + continue; + } + UIImage *_Nullable image = [UIImage imageWithContentsOfFile:thumbnailPath]; + return image; + } + + [OWSThumbnailService.shared ensureThumbnailForAttachmentIdWithAttachmentId:self.uniqueId + thumbnailDimensionPoints:thumbnailDimensionPoints + completion:completion]; + return nil; +} + #pragma mark - Update With... Methods - (void)markForLazyRestoreWithFragment:(OWSBackupFragment *)lazyRestoreFragment @@ -783,6 +900,40 @@ const CGFloat kMaxVideoStillSize = 1 * 1024; return thumbnailAttachment; } +- (void)updateWithNewThumbnail:(NSString *)tempFilePath + thumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints + size:(CGSize)size + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssert(tempFilePath.length > 0); + OWSAssert(thumbnailDimensionPoints > 0); + OWSAssert(size.width > 0 && size.height); + OWSAssert(transaction); + + NSString *filename = tempFilePath.lastPathComponent; + NSString *containingDir = self.originalFilePath.stringByDeletingLastPathComponent; + NSString *filePath = [containingDir stringByAppendingPathComponent:filename]; + + NSError *_Nullable error; + BOOL success = [[NSFileManager defaultManager] moveItemAtPath:tempFilePath toPath:filePath error:&error]; + if (error || !success) { + OWSFail(@"Could not move new thumbnail image: %@.", error); + return; + } + TSAttachmentThumbnail *newThumbnail = [[TSAttachmentThumbnail alloc] initWithFilename:filename + size:size + thumbnailDimensionPoints:thumbnailDimensionPoints]; + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSAttachmentStream *attachment) { + NSMutableArray *thumbnails + = (attachment.thumbnails ? [attachment.thumbnails mutableCopy] + : [NSMutableArray new]); + [thumbnails addObject:newThumbnail]; + [attachment setThumbnails:thumbnails]; + }]; +} + // MARK: Protobuf serialization + (nullable SSKProtoAttachmentPointer *)buildProtoForAttachmentId:(nullable NSString *)attachmentId diff --git a/SignalMessaging/utils/SwiftSingletons.swift b/SignalServiceKit/src/Util/SwiftSingletons.swift similarity index 86% rename from SignalMessaging/utils/SwiftSingletons.swift rename to SignalServiceKit/src/Util/SwiftSingletons.swift index cccfa2e7e..d02ad65a2 100644 --- a/SignalMessaging/utils/SwiftSingletons.swift +++ b/SignalServiceKit/src/Util/SwiftSingletons.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation @@ -20,7 +20,7 @@ public class SwiftSingletons: NSObject { guard _isDebugAssertConfiguration() else { return } - let singletonClassName = String(describing:type(of:singleton)) + let singletonClassName = String(describing: type(of: singleton)) guard !classSet.contains(singletonClassName) else { owsFail("\(self.logTag) in \(#function) Duplicate singleton: \(singletonClassName).") return diff --git a/SignalServiceKit/src/Util/UIImage+OWS.h b/SignalServiceKit/src/Util/UIImage+OWS.h index 35eb5d673..f8e08a8b2 100644 --- a/SignalServiceKit/src/Util/UIImage+OWS.h +++ b/SignalServiceKit/src/Util/UIImage+OWS.h @@ -9,7 +9,7 @@ NS_ASSUME_NONNULL_BEGIN - (UIImage *)normalizedImage; - (UIImage *)resizedWithQuality:(CGInterpolationQuality)quality rate:(CGFloat)rate; -- (UIImage *)resizedImageToSize:(CGSize)dstSize; +- (nullable UIImage *)resizedImageToSize:(CGSize)dstSize; - (UIImage *)resizedImageToFillPixelSize:(CGSize)boundingSize; + (UIImage *)imageWithColor:(UIColor *)color; diff --git a/SignalServiceKit/src/Util/UIImage+OWS.m b/SignalServiceKit/src/Util/UIImage+OWS.m index 300fd07e3..2f67225fd 100644 --- a/SignalServiceKit/src/Util/UIImage+OWS.m +++ b/SignalServiceKit/src/Util/UIImage+OWS.m @@ -37,7 +37,7 @@ // Source: https://github.com/AliSoftware/UIImage-Resize -- (UIImage *)resizedImageToSize:(CGSize)dstSize +- (nullable UIImage *)resizedImageToSize:(CGSize)dstSize { CGImageRef imgRef = self.CGImage; // the below values are regardless of orientation : for UIImages from Camera, width>height (landscape) @@ -106,10 +106,10 @@ UIGraphicsBeginImageContextWithOptions(dstSize, NO, self.scale); CGContextRef context = UIGraphicsGetCurrentContext(); - if (!context) { return nil; } + CGContextSetInterpolationQuality(context, kCGInterpolationHigh); if (orient == UIImageOrientationRight || orient == UIImageOrientationLeft) { CGContextScaleCTM(context, -scaleRatio, scaleRatio); @@ -124,7 +124,7 @@ // we use srcSize (and not dstSize) as the size to specify is in user space (and we use the CTM to apply a // scaleRatio) CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, srcSize.width, srcSize.height), imgRef); - UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIImage *_Nullable resizedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return resizedImage;