diff --git a/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.h b/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.h index 6864722f8..81cb10710 100644 --- a/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.h +++ b/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.h @@ -10,4 +10,8 @@ - (CGSize)ows_adjustBubbleSize:(CGSize)bubbleSize forImage:(UIImage *)image; +- (CGSize)ows_adjustBubbleSize:(CGSize)bubbleSize forImageSize:(CGSize)imageSize; + +- (CGSize)sizeOfImageAtURL:(NSURL *)imageURL; + @end diff --git a/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.m b/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.m index 63900bfe7..da6ff7cfe 100644 --- a/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.m +++ b/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.m @@ -3,8 +3,9 @@ // #import "JSQMediaItem+OWS.h" -#import "UIDevice+TSHardwareVersion.h" #import "NumberUtil.h" +#import "UIDevice+TSHardwareVersion.h" +#import @implementation JSQMediaItem (OWS) @@ -15,7 +16,12 @@ } - (CGSize)ows_adjustBubbleSize:(CGSize)bubbleSize forImage:(UIImage *)image { - double aspectRatio = image.size.height / image.size.width; + return [self ows_adjustBubbleSize:bubbleSize forImageSize:image.size]; +} + +- (CGSize)ows_adjustBubbleSize:(CGSize)bubbleSize forImageSize:(CGSize)imageSize +{ + double aspectRatio = imageSize.height / imageSize.width; double clampedAspectRatio = [NumberUtil clamp:aspectRatio toMin:0.5 andMax:1.5]; if ([[UIDevice currentDevice] isiPhoneVersionSixOrMore]) { @@ -32,4 +38,34 @@ return bubbleSize; } +- (CGSize)sizeOfImageAtURL:(NSURL *)imageURL +{ + OWSAssert(imageURL); + + // With CGImageSource we avoid loading the whole image into memory. + CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)imageURL, NULL); + if (!source) { + OWSAssert(0); + return CGSizeZero; + } + + NSDictionary *options = @{ + (NSString *)kCGImageSourceShouldCache : @(NO), + }; + NSDictionary *properties + = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, (CFDictionaryRef)options); + CGSize imageSize = CGSizeZero; + if (properties) { + NSNumber *width = properties[(NSString *)kCGImagePropertyPixelWidth]; + NSNumber *height = properties[(NSString *)kCGImagePropertyPixelHeight]; + if (width && height) { + imageSize = CGSizeMake(width.floatValue, height.floatValue); + } else { + OWSAssert(0); + } + } + CFRelease(source); + return imageSize; +} + @end diff --git a/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.h b/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.h index f42c4fd61..4d2456c9d 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.h +++ b/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.h @@ -12,10 +12,9 @@ NS_ASSUME_NONNULL_BEGIN @interface TSAnimatedAdapter : JSQMediaItem -- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming; +@property (nonatomic, readonly) NSString *attachmentId; -@property NSString *attachmentId; -@property NSData *fileData; +- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming; @end diff --git a/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m index 225b8ec55..7e8222b45 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m @@ -15,13 +15,13 @@ NS_ASSUME_NONNULL_BEGIN @interface TSAnimatedAdapter () -// @property (nonatomic, nullable) FLAnimatedImageView *cachedImageView; -@property (nonatomic) UIImage *image; @property (nonatomic) TSAttachmentStream *attachment; @property (nonatomic, nullable) AttachmentUploadView *attachmentUploadView; @property (nonatomic) BOOL incoming; +@property (nonatomic) CGSize imageSize; +@property (nonatomic) NSString *attachmentId; // See comments on OWSMessageMediaAdapter. @property (nonatomic, nullable, weak) id lastPresentingCell; @@ -40,9 +40,8 @@ NS_ASSUME_NONNULL_BEGIN _cachedImageView = nil; _attachment = attachment; _attachmentId = attachment.uniqueId; - _image = [attachment image]; - _fileData = [NSData dataWithContentsOfURL:[attachment mediaURL]]; _incoming = incoming; + _imageSize = attachment.mediaURL ? [self sizeOfImageAtURL:attachment.mediaURL] : CGSizeZero; } return self; @@ -50,6 +49,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)clearAllViews { + OWSAssert([NSThread isMainThread]); + [_cachedImageView removeFromSuperview]; _cachedImageView = nil; _attachmentUploadView = nil; @@ -69,7 +70,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSUInteger)hash { - return super.hash ^ self.image.hash; + return super.hash ^ self.attachment.uniqueId.hash; } #pragma mark - OWSMessageMediaAdapter @@ -95,9 +96,17 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - JSQMessageMediaData protocol - (UIView *)mediaView { + OWSAssert([NSThread isMainThread]); + if (self.cachedImageView == nil) { // Use Flipboard FLAnimatedImage library to display gifs - FLAnimatedImage *animatedGif = [FLAnimatedImage animatedImageWithGIFData:self.fileData]; + NSData *fileData = [NSData dataWithContentsOfURL:[self.attachment mediaURL]]; + if (!fileData) { + DDLogError(@"%@ Could not load image: %@", [self tag], [self.attachment mediaURL]); + OWSAssert(0); + return nil; + } + FLAnimatedImage *animatedGif = [FLAnimatedImage animatedImageWithGIFData:fileData]; FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init]; imageView.animatedImage = animatedGif; CGSize size = [self mediaViewDisplaySize]; @@ -119,7 +128,7 @@ NS_ASSUME_NONNULL_BEGIN } - (CGSize)mediaViewDisplaySize { - return [self ows_adjustBubbleSize:[super mediaViewDisplaySize] forImage:self.image]; + return [self ows_adjustBubbleSize:[super mediaViewDisplaySize] forImageSize:self.imageSize]; } #pragma mark - OWSMessageEditing Protocol @@ -138,12 +147,22 @@ NS_ASSUME_NONNULL_BEGIN utiType = (NSString *)kUTTypeGIF; } - UIPasteboard *pasteboard = UIPasteboard.generalPasteboard; - [pasteboard setData:self.fileData forPasteboardType:utiType]; + NSData *data = [NSData dataWithContentsOfURL:[self.attachment mediaURL]]; + if (!data) { + DDLogError(@"%@ Could not load image data: %@", [self tag], [self.attachment mediaURL]); + OWSAssert(0); + return; + } + [UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType]; } else if (action == NSSelectorFromString(@"save:")) { - NSData *photoData = self.fileData; + NSData *data = [NSData dataWithContentsOfURL:[self.attachment mediaURL]]; + if (!data) { + DDLogError(@"%@ Could not load image data: %@", [self tag], [self.attachment mediaURL]); + OWSAssert(0); + return; + } ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; - [library writeImageDataToSavedPhotosAlbum:photoData + [library writeImageDataToSavedPhotosAlbum:data metadata:nil completionBlock:^(NSURL *assetURL, NSError *error) { if (error) { @@ -157,6 +176,18 @@ NS_ASSUME_NONNULL_BEGIN } } +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/Models/TSMessageAdapaters/TSPhotoAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSPhotoAdapter.m index d1f6614d5..34faa6185 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSPhotoAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSPhotoAdapter.m @@ -6,6 +6,7 @@ #import "AttachmentUploadView.h" #import "JSQMediaItem+OWS.h" #import "TSAttachmentStream.h" +#import #import #import #import @@ -17,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) UIImageView *cachedImageView; @property (nonatomic, nullable) AttachmentUploadView *attachmentUploadView; @property (nonatomic) BOOL incoming; +@property (nonatomic) CGSize imageSize; // See comments on OWSMessageMediaAdapter. @property (nonatomic, nullable, weak) id lastPresentingCell; @@ -29,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming { - self = [super initWithImage:attachment.image]; + self = [super init]; if (!self) { return self; @@ -39,12 +41,15 @@ NS_ASSUME_NONNULL_BEGIN _attachment = attachment; _attachmentId = attachment.uniqueId; _incoming = incoming; + _imageSize = attachment.mediaURL ? [self sizeOfImageAtURL:attachment.mediaURL] : CGSizeZero; return self; } - (void)clearAllViews { + OWSAssert([NSThread isMainThread]); + [_cachedImageView removeFromSuperview]; _cachedImageView = nil; _attachmentUploadView = nil; @@ -64,13 +69,17 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - JSQMessageMediaData protocol - (UIView *)mediaView { - if (self.image == nil) { - return nil; - } + OWSAssert([NSThread isMainThread]); if (self.cachedImageView == nil) { + UIImage *image = self.attachment.image; + if (!image) { + DDLogError(@"%@ Could not load image: %@", [self tag], [self.attachment mediaURL]); + OWSAssert(0); + return nil; + } CGSize size = [self mediaViewDisplaySize]; - UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image]; + UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; imageView.contentMode = UIViewContentModeScaleAspectFill; imageView.frame = CGRectMake(0.0f, 0.0f, size.width, size.height); imageView.clipsToBounds = YES; @@ -93,7 +102,7 @@ NS_ASSUME_NONNULL_BEGIN } - (CGSize)mediaViewDisplaySize { - return [self ows_adjustBubbleSize:[super mediaViewDisplaySize] forImage:self.image]; + return [self ows_adjustBubbleSize:[super mediaViewDisplaySize] forImageSize:self.imageSize]; } #pragma mark - OWSMessageEditing Protocol @@ -106,14 +115,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)performEditingAction:(SEL)action { NSString *actionString = NSStringFromSelector(action); - if (!self.image) { - OWSAssert(NO); - DDLogWarn(@"Refusing to perform '%@' action with nil image for %@: attachmentId=%@. (corrupted attachment?)", - actionString, - self.class, - self.attachmentId); - return; - } if (action == @selector(copy:)) { // We should always copy to the pasteboard as data, not an UIImage. @@ -126,10 +127,28 @@ NS_ASSUME_NONNULL_BEGIN utiType = (NSString *)kUTTypeImage; } NSData *data = [NSData dataWithContentsOfURL:self.attachment.mediaURL]; + if (!data) { + DDLogError(@"%@ Could not load image data: %@", [self tag], [self.attachment mediaURL]); + OWSAssert(0); + return; + } [UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType]; return; } else if (action == NSSelectorFromString(@"save:")) { - UIImageWriteToSavedPhotosAlbum(self.image, nil, nil, nil); + NSData *data = [NSData dataWithContentsOfURL:[self.attachment mediaURL]]; + if (!data) { + DDLogError(@"%@ Could not load image data: %@", [self tag], [self.attachment mediaURL]); + OWSAssert(0); + return; + } + ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; + [library writeImageDataToSavedPhotosAlbum:data + metadata:nil + completionBlock:^(NSURL *assetURL, NSError *error) { + if (error) { + DDLogWarn(@"Error Saving image to photo album: %@", error); + } + }]; return; } @@ -154,6 +173,18 @@ NS_ASSUME_NONNULL_BEGIN } } +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/Models/TSMessageAdapaters/TSVideoAttachmentAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSVideoAttachmentAdapter.m index 64bc837e7..79bb077f8 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSVideoAttachmentAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSVideoAttachmentAdapter.m @@ -320,9 +320,9 @@ NS_ASSUME_NONNULL_BEGIN [bottomLabel sizeToFit]; [mediaView addSubview:bottomLabel]; - const CGFloat topLabelHeight = ceil(topLabel.font.lineHeight); + const CGFloat topLabelHeight = (CGFloat)ceil(topLabel.font.lineHeight); const CGFloat kAudioProgressViewHeight = 12.f; - const CGFloat bottomLabelHeight = ceil(bottomLabel.font.lineHeight); + const CGFloat bottomLabelHeight = (CGFloat)ceil(bottomLabel.font.lineHeight); CGRect labelsBounds = CGRectZero; labelsBounds.origin.x = (CGFloat)round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing); labelsBounds.size.width = contentFrame.origin.x + contentFrame.size.width - labelsBounds.origin.x;