Merge branch 'charlesmchen/convoThumbnails' into release/2.29.2

pull/1/head
Matthew Chen 7 years ago
commit 9ad661c29f

@ -1 +1 @@
Subproject commit 9a3f6876d4a6086d10501383b96fb2d9d47a75b6 Subproject commit f594e0655b038d7a809b2bcc0222a00e64586d9f

@ -148,7 +148,6 @@
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; }; 3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; };
347850551FD749C0007B8332 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; }; 347850551FD749C0007B8332 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
347850571FD86544007B8332 /* SAEFailedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850561FD86544007B8332 /* SAEFailedViewController.swift */; }; 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 */; }; 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, ); }; }; 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 */; }; 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; }; 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 = "<group>"; }; 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = "<group>"; };
347850561FD86544007B8332 /* SAEFailedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEFailedViewController.swift; sourceTree = "<group>"; }; 347850561FD86544007B8332 /* SAEFailedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEFailedViewController.swift; sourceTree = "<group>"; };
347850581FD9972E007B8332 /* SwiftSingletons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSingletons.swift; sourceTree = "<group>"; };
3478505A1FD999D5007B8332 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = translations/et.lproj/Localizable.strings; sourceTree = "<group>"; }; 3478505A1FD999D5007B8332 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = translations/et.lproj/Localizable.strings; sourceTree = "<group>"; };
3478505C1FD99A1F007B8332 /* zh_TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_TW; path = translations/zh_TW.lproj/Localizable.strings; sourceTree = "<group>"; }; 3478505C1FD99A1F007B8332 /* zh_TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_TW; path = translations/zh_TW.lproj/Localizable.strings; sourceTree = "<group>"; };
347850651FD9B789007B8332 /* AppSetup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppSetup.m; sourceTree = "<group>"; }; 347850651FD9B789007B8332 /* AppSetup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppSetup.m; sourceTree = "<group>"; };
@ -1559,7 +1557,6 @@
34641E1120878FB000E2EDE5 /* OWSWindowManager.h */, 34641E1120878FB000E2EDE5 /* OWSWindowManager.h */,
34641E1020878FAF00E2EDE5 /* OWSWindowManager.m */, 34641E1020878FAF00E2EDE5 /* OWSWindowManager.m */,
45360B8C1F9521F800FA666C /* Searcher.swift */, 45360B8C1F9521F800FA666C /* Searcher.swift */,
347850581FD9972E007B8332 /* SwiftSingletons.swift */,
346129BD1FD2068600532771 /* ThreadUtil.h */, 346129BD1FD2068600532771 /* ThreadUtil.h */,
346129BE1FD2068600532771 /* ThreadUtil.m */, 346129BE1FD2068600532771 /* ThreadUtil.m */,
B97940251832BD2400BD66CB /* UIUtil.h */, B97940251832BD2400BD66CB /* UIUtil.h */,
@ -3208,7 +3205,6 @@
346129AB1FD1F0EE00532771 /* OWSFormat.m in Sources */, 346129AB1FD1F0EE00532771 /* OWSFormat.m in Sources */,
34AC0A12211B39EA00997B47 /* ContactTableViewCell.m in Sources */, 34AC0A12211B39EA00997B47 /* ContactTableViewCell.m in Sources */,
451F8A461FD715BA005CB9DA /* OWSGroupAvatarBuilder.m in Sources */, 451F8A461FD715BA005CB9DA /* OWSGroupAvatarBuilder.m in Sources */,
347850591FD9972E007B8332 /* SwiftSingletons.swift in Sources */,
346129961FD1E30000532771 /* OWSDatabaseMigration.m in Sources */, 346129961FD1E30000532771 /* OWSDatabaseMigration.m in Sources */,
346129FB1FD5F31400532771 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */, 346129FB1FD5F31400532771 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */,
34AC09EA211B39B100997B47 /* ModalActivityIndicatorViewController.swift in Sources */, 34AC09EA211B39B100997B47 /* ModalActivityIndicatorViewController.swift in Sources */,

@ -192,7 +192,7 @@ NS_ASSUME_NONNULL_BEGIN
NSString *filename = self.attachmentStream.sourceFilename; NSString *filename = self.attachmentStream.sourceFilename;
if (!filename) { if (!filename) {
filename = [[self.attachmentStream filePath] lastPathComponent]; filename = [self.attachmentStream.originalFilePath lastPathComponent];
} }
NSString *topText = [[filename stringByDeletingPathExtension] ows_stripped]; NSString *topText = [[filename stringByDeletingPathExtension] ows_stripped];
if (topText.length < 1) { if (topText.length < 1) {

@ -107,7 +107,7 @@ NS_ASSUME_NONNULL_BEGIN
NSString *filename = self.attachmentStream.sourceFilename; NSString *filename = self.attachmentStream.sourceFilename;
if (!filename) { if (!filename) {
filename = [[self.attachmentStream filePath] lastPathComponent]; filename = [[self.attachmentStream originalFilePath] lastPathComponent];
} }
NSString *fileExtension = filename.pathExtension; NSString *fileExtension = filename.pathExtension;
if (fileExtension.length < 1) { if (fileExtension.length < 1) {
@ -149,7 +149,8 @@ NS_ASSUME_NONNULL_BEGIN
NSError *error; NSError *error;
unsigned long long fileSize = unsigned long long fileSize =
[[NSFileManager defaultManager] attributesOfItemAtPath:[self.attachmentStream filePath] error:&error].fileSize; [[NSFileManager defaultManager] attributesOfItemAtPath:[self.attachmentStream originalFilePath] error:&error]
.fileSize;
OWSAssert(!error); OWSAssert(!error);
NSString *bottomText = [OWSFormat formatFileSize:fileSize]; NSString *bottomText = [OWSFormat formatFileSize:fileSize];
UILabel *bottomLabel = [UILabel new]; UILabel *bottomLabel = [UILabel new];

@ -655,6 +655,7 @@ NS_ASSUME_NONNULL_BEGIN
mediaView:(UIView *)mediaView mediaView:(UIView *)mediaView
cacheKey:(NSString *)cacheKey cacheKey:(NSString *)cacheKey
shouldSkipCache:(BOOL)shouldSkipCache shouldSkipCache:(BOOL)shouldSkipCache
canLoadAsync:(BOOL)canLoadAsync
{ {
OWSAssert(self.attachmentStream); OWSAssert(self.attachmentStream);
OWSAssert(mediaView); OWSAssert(mediaView);
@ -676,8 +677,8 @@ NS_ASSUME_NONNULL_BEGIN
if (!shouldSkipCache) { if (!shouldSkipCache) {
[self.cellMediaCache setObject:cellMedia forKey:cacheKey]; [self.cellMediaCache setObject:cellMedia forKey:cacheKey];
} }
} else { } else if (!canLoadAsync) {
DDLogError(@"%@ Failed to load cell media: %@", [self logTag], [self.attachmentStream mediaURL]); DDLogError(@"%@ Failed to load cell media: %@", [self logTag], [self.attachmentStream originalMediaURL]);
self.viewItem.didCellMediaFailToLoad = YES; self.viewItem.didCellMediaFailToLoad = YES;
[self showAttachmentErrorViewWithMediaView:mediaView]; [self showAttachmentErrorViewWithMediaView:mediaView];
} }
@ -836,6 +837,8 @@ NS_ASSUME_NONNULL_BEGIN
stillImageView.backgroundColor = [UIColor whiteColor]; stillImageView.backgroundColor = [UIColor whiteColor];
[self addAttachmentUploadViewIfNecessary]; [self addAttachmentUploadViewIfNecessary];
__weak UIImageView *weakImageView = stillImageView;
__weak OWSMessageBubbleView *weakSelf = self; __weak OWSMessageBubbleView *weakSelf = self;
self.loadCellContentBlock = ^{ self.loadCellContentBlock = ^{
OWSMessageBubbleView *strongSelf = weakSelf; OWSMessageBubbleView *strongSelf = weakSelf;
@ -851,14 +854,23 @@ NS_ASSUME_NONNULL_BEGIN
// TODO: Don't use full size images in the message cells. // TODO: Don't use full size images in the message cells.
const NSUInteger kMaxCachableSize = 1024 * 1024; const NSUInteger kMaxCachableSize = 1024 * 1024;
BOOL shouldSkipCache = BOOL shouldSkipCache =
[OWSFileSystem fileSizeOfPath:strongSelf.attachmentStream.filePath].unsignedIntegerValue < kMaxCachableSize; [OWSFileSystem fileSizeOfPath:strongSelf.attachmentStream.originalFilePath].unsignedIntegerValue
stillImageView.image = [strongSelf tryToLoadCellMedia:^{ < kMaxCachableSize;
stillImageView.image = [strongSelf
tryToLoadCellMedia:^{
OWSCAssert([strongSelf.attachmentStream isImage]); OWSCAssert([strongSelf.attachmentStream isImage]);
return strongSelf.attachmentStream.image; return [strongSelf.attachmentStream
thumbnailImageMediumWithSuccess:^(UIImage *image) {
weakImageView.image = image;
}
failure:^{
DDLogError(@"Could not load thumbnail.");
}];
} }
mediaView:stillImageView mediaView:stillImageView
cacheKey:strongSelf.attachmentStream.uniqueId cacheKey:strongSelf.attachmentStream.uniqueId
shouldSkipCache:shouldSkipCache]; shouldSkipCache:shouldSkipCache
canLoadAsync:YES];
}; };
self.unloadCellContentBlock = ^{ self.unloadCellContentBlock = ^{
OWSMessageBubbleView *strongSelf = weakSelf; OWSMessageBubbleView *strongSelf = weakSelf;
@ -894,10 +906,11 @@ NS_ASSUME_NONNULL_BEGIN
if (animatedImageView.image) { if (animatedImageView.image) {
return; return;
} }
animatedImageView.image = [strongSelf tryToLoadCellMedia:^{ animatedImageView.image = [strongSelf
tryToLoadCellMedia:^{
OWSCAssert([strongSelf.attachmentStream isAnimated]); OWSCAssert([strongSelf.attachmentStream isAnimated]);
NSString *_Nullable filePath = [strongSelf.attachmentStream filePath]; NSString *_Nullable filePath = [strongSelf.attachmentStream originalFilePath];
YYImage *_Nullable animatedImage = nil; YYImage *_Nullable animatedImage = nil;
if (strongSelf.attachmentStream.isValidImage && filePath) { if (strongSelf.attachmentStream.isValidImage && filePath) {
animatedImage = [YYImage imageWithContentsOfFile:filePath]; animatedImage = [YYImage imageWithContentsOfFile:filePath];
@ -906,7 +919,8 @@ NS_ASSUME_NONNULL_BEGIN
} }
mediaView:animatedImageView mediaView:animatedImageView
cacheKey:strongSelf.attachmentStream.uniqueId cacheKey:strongSelf.attachmentStream.uniqueId
shouldSkipCache:NO]; shouldSkipCache:NO
canLoadAsync:NO];
}; };
self.unloadCellContentBlock = ^{ self.unloadCellContentBlock = ^{
OWSMessageBubbleView *strongSelf = weakSelf; OWSMessageBubbleView *strongSelf = weakSelf;
@ -975,14 +989,16 @@ NS_ASSUME_NONNULL_BEGIN
if (stillImageView.image) { if (stillImageView.image) {
return; return;
} }
stillImageView.image = [strongSelf tryToLoadCellMedia:^{ stillImageView.image = [strongSelf
tryToLoadCellMedia:^{
OWSCAssert([strongSelf.attachmentStream isVideo]); OWSCAssert([strongSelf.attachmentStream isVideo]);
return strongSelf.attachmentStream.image; return strongSelf.attachmentStream.originalImage;
} }
mediaView:stillImageView mediaView:stillImageView
cacheKey:strongSelf.attachmentStream.uniqueId cacheKey:strongSelf.attachmentStream.uniqueId
shouldSkipCache:NO]; shouldSkipCache:NO
canLoadAsync:NO];
}; };
self.unloadCellContentBlock = ^{ self.unloadCellContentBlock = ^{
OWSMessageBubbleView *strongSelf = weakSelf; OWSMessageBubbleView *strongSelf = weakSelf;

@ -2219,8 +2219,8 @@ typedef enum : NSUInteger {
OWSAssert(attachmentStream); OWSAssert(attachmentStream);
NSFileManager *fileManager = [NSFileManager defaultManager]; NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:[attachmentStream.mediaURL path]]) { if (![fileManager fileExistsAtPath:attachmentStream.originalFilePath]) {
OWSFail(@"%@ Missing video file: %@", self.logTag, attachmentStream.mediaURL); OWSFail(@"%@ Missing video file: %@", self.logTag, attachmentStream.originalFilePath);
} }
[self dismissKeyBoard]; [self dismissKeyBoard];
@ -2235,7 +2235,8 @@ typedef enum : NSUInteger {
[self.audioAttachmentPlayer stop]; [self.audioAttachmentPlayer stop];
self.audioAttachmentPlayer = nil; self.audioAttachmentPlayer = nil;
} }
self.audioAttachmentPlayer = [[OWSAudioPlayer alloc] initWithMediaUrl:attachmentStream.mediaURL delegate:viewItem]; self.audioAttachmentPlayer =
[[OWSAudioPlayer alloc] initWithMediaUrl:attachmentStream.originalMediaURL delegate:viewItem];
// Associate the player with this media adapter. // Associate the player with this media adapter.
self.audioAttachmentPlayer.owner = viewItem; self.audioAttachmentPlayer.owner = viewItem;
[self.audioAttachmentPlayer playWithPlaybackAudioCategory]; [self.audioAttachmentPlayer playWithPlaybackAudioCategory];

@ -382,7 +382,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
return [self displayableTextForCacheKey:displayableTextCacheKey return [self displayableTextForCacheKey:displayableTextCacheKey
textBlock:^{ textBlock:^{
NSData *textData = [NSData dataWithContentsOfURL:attachmentStream.mediaURL]; NSData *textData =
[NSData dataWithContentsOfURL:attachmentStream.originalMediaURL];
NSString *text = NSString *text =
[[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding]; [[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding];
return text; return text;
@ -733,7 +734,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
OWSFail(@"%@ Unknown MIME type: %@", self.logTag, self.attachmentStream.contentType); OWSFail(@"%@ Unknown MIME type: %@", self.logTag, self.attachmentStream.contentType);
utiType = (NSString *)kUTTypeGIF; utiType = (NSString *)kUTTypeGIF;
} }
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream mediaURL]]; NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream originalMediaURL]];
if (!data) { if (!data) {
OWSFail(@"%@ Could not load attachment data", self.logTag); OWSFail(@"%@ Could not load attachment data", self.logTag);
return; return;
@ -814,7 +815,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
case OWSMessageCellType_Audio: case OWSMessageCellType_Audio:
return NO; return NO;
case OWSMessageCellType_Video: case OWSMessageCellType_Video:
return UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.mediaURL.path); return UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.originalFilePath);
case OWSMessageCellType_GenericAttachment: case OWSMessageCellType_GenericAttachment:
return NO; return NO;
case OWSMessageCellType_DownloadingAttachment: { case OWSMessageCellType_DownloadingAttachment: {
@ -834,7 +835,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
break; break;
case OWSMessageCellType_StillImage: case OWSMessageCellType_StillImage:
case OWSMessageCellType_AnimatedImage: { case OWSMessageCellType_AnimatedImage: {
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream mediaURL]]; NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream originalMediaURL]];
if (!data) { if (!data) {
OWSFail(@"%@ Could not load image data", self.logTag); OWSFail(@"%@ Could not load image data", self.logTag);
return; return;
@ -853,8 +854,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
OWSFail(@"%@ Cannot save media data.", self.logTag); OWSFail(@"%@ Cannot save media data.", self.logTag);
break; break;
case OWSMessageCellType_Video: case OWSMessageCellType_Video:
if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.mediaURL.path)) { if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.originalFilePath)) {
UISaveVideoAtPathToSavedPhotosAlbum(self.attachmentStream.mediaURL.path, self, nil, nil); UISaveVideoAtPathToSavedPhotosAlbum(self.attachmentStream.originalFilePath, self, nil, nil);
} else { } else {
OWSFail(@"%@ Could not save incompatible video data.", self.logTag); OWSFail(@"%@ Could not save incompatible video data.", self.logTag);
} }

@ -73,7 +73,7 @@ NS_ASSUME_NONNULL_BEGIN
_galleryItemBox = galleryItemBox; _galleryItemBox = galleryItemBox;
_viewItem = viewItem; _viewItem = viewItem;
// We cache the image data in case the attachment stream is deleted. // We cache the image data in case the attachment stream is deleted.
_image = galleryItemBox.attachmentStream.image; _image = galleryItemBox.attachmentStream.originalImage;
return self; return self;
} }
@ -85,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
- (NSURL *_Nullable)attachmentUrl - (NSURL *_Nullable)attachmentUrl
{ {
return self.attachmentStream.mediaURL; return self.attachmentStream.originalMediaURL;
} }
- (NSData *)fileData - (NSData *)fileData

@ -22,28 +22,24 @@ public struct MediaGalleryItem: Equatable, Hashable {
} }
var isVideo: Bool { var isVideo: Bool {
return attachmentStream.isVideo() return attachmentStream.isVideo
} }
var isAnimated: Bool { var isAnimated: Bool {
return attachmentStream.isAnimated() return attachmentStream.isAnimated
} }
var isImage: Bool { var isImage: Bool {
return attachmentStream.isImage() return attachmentStream.isImage
} }
var thumbnailImage: UIImage { public typealias AsyncThumbnailBlock = (UIImage) -> Void
guard let image = attachmentStream.thumbnailImage() else { func thumbnailImage(async:@escaping AsyncThumbnailBlock) -> UIImage? {
owsFail("\(logTag) in \(#function) unexpectedly unable to build attachment thumbnail") return attachmentStream.thumbnailImageSmall(success: async, failure: {})
return UIImage()
}
return image
} }
var fullSizedImage: UIImage { var fullSizedImage: UIImage {
guard let image = attachmentStream.image() else { guard let image = attachmentStream.originalImage else {
owsFail("\(logTag) in \(#function) unexpectedly unable to build attachment image") owsFail("\(logTag) in \(#function) unexpectedly unable to build attachment image")
return UIImage() return UIImage()
} }

@ -922,7 +922,24 @@ private class MediaGalleryCell: UICollectionViewCell {
public func configure(item: MediaGalleryItem) { public func configure(item: MediaGalleryItem) {
self.item = item self.item = item
self.imageView.image = item.thumbnailImage if let image = item.thumbnailImage(async: {
[weak self] (image) in
guard let strongSelf = self else {
return
}
guard strongSelf.item == item else {
return
}
strongSelf.imageView.image = image
strongSelf.imageView.backgroundColor = UIColor.clear
}) {
self.imageView.image = image
self.imageView.backgroundColor = UIColor.clear
} else {
// TODO: Show a placeholder?
self.imageView.backgroundColor = Theme.offBackgroundColor
}
if item.isVideo { if item.isVideo {
self.contentTypeBadgeView.isHidden = false self.contentTypeBadgeView.isHidden = false
self.contentTypeBadgeView.image = MediaGalleryCell.videoBadgeImage self.contentTypeBadgeView.image = MediaGalleryCell.videoBadgeImage

@ -651,7 +651,7 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
func didTapAudioViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream) { func didTapAudioViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream) {
SwiftAssertIsOnMainThread(#function) SwiftAssertIsOnMainThread(#function)
guard let mediaURL = attachmentStream.mediaURL() else { guard let mediaURL = attachmentStream.originalMediaURL else {
owsFail("\(logTag) in \(#function) mediaURL was unexpectedly nil for attachment: \(attachmentStream)") owsFail("\(logTag) in \(#function) mediaURL was unexpectedly nil for attachment: \(attachmentStream)")
return return
} }

@ -546,7 +546,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(backupIO); OWSAssert(backupIO);
OWSAssert(completion); OWSAssert(completion);
NSString *_Nullable attachmentFilePath = [attachment filePath]; NSString *_Nullable attachmentFilePath = [attachment originalFilePath];
if (attachmentFilePath.length < 1) { if (attachmentFilePath.length < 1) {
DDLogError(@"%@ Attachment has invalid file path.", self.logTag); DDLogError(@"%@ Attachment has invalid file path.", self.logTag);
return completion(NO); return completion(NO);
@ -617,7 +617,7 @@ NS_ASSUME_NONNULL_BEGIN
} }
} }
NSString *_Nullable attachmentFilePath = [attachment filePath]; NSString *_Nullable attachmentFilePath = [attachment originalFilePath];
if (attachmentFilePath.length < 1) { if (attachmentFilePath.length < 1) {
DDLogError(@"%@ Attachment has invalid file path.", self.logTag); DDLogError(@"%@ Attachment has invalid file path.", self.logTag);
return completion(NO); return completion(NO);

@ -539,7 +539,7 @@ NS_ASSUME_NONNULL_BEGIN
return NO; return NO;
} }
TSAttachmentStream *attachmentStream = object; TSAttachmentStream *attachmentStream = object;
NSString *_Nullable filePath = attachmentStream.filePath; NSString *_Nullable filePath = attachmentStream.originalFilePath;
if (!filePath) { if (!filePath) {
DDLogError(@"%@ attachment is missing file.", self.logTag); DDLogError(@"%@ attachment is missing file.", self.logTag);
return NO; return NO;

@ -316,17 +316,15 @@ typedef void (^OrphanDataBlock)(OWSOrphanData *);
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
attachmentStreamCount++; attachmentStreamCount++;
NSString *_Nullable filePath = [attachmentStream filePath]; NSString *_Nullable filePath = [attachmentStream originalFilePath];
if (filePath) { if (filePath) {
[allAttachmentFilePaths addObject:filePath]; [allAttachmentFilePaths addObject:filePath];
} else { } else {
OWSFail(@"%@ attachment has no file path.", self.logTag); OWSFail(@"%@ attachment has no file path.", self.logTag);
} }
NSString *_Nullable thumbnailPath = [attachmentStream thumbnailPath]; [allAttachmentFilePaths
if (thumbnailPath.length > 0) { addObjectsFromArray:attachmentStream.allThumbnailPaths];
[allAttachmentFilePaths addObject:thumbnailPath];
}
}]; }];
if (shouldAbort) { if (shouldAbort) {

@ -126,7 +126,7 @@
TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId]; TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]); XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]);
TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment; TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment;
NSString *_Nullable filePath = attachmentStream.filePath; NSString *_Nullable filePath = attachmentStream.originalFilePath;
XCTAssertNotNil(filePath); XCTAssertNotNil(filePath);
XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]); XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]);
@ -148,7 +148,7 @@
TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId]; TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]); XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]);
TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment; TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment;
NSString *_Nullable filePath = attachmentStream.filePath; NSString *_Nullable filePath = attachmentStream.originalFilePath;
XCTAssertNotNil(filePath); XCTAssertNotNil(filePath);
XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]); XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]);
@ -170,7 +170,7 @@
TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId]; TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]); XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]);
TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment; TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment;
NSString *_Nullable filePath = attachmentStream.filePath; NSString *_Nullable filePath = attachmentStream.originalFilePath;
XCTAssertNotNil(filePath); XCTAssertNotNil(filePath);
XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]); XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]);
@ -192,7 +192,7 @@
TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId]; TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]); XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]);
TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment; TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment;
NSString *_Nullable filePath = attachmentStream.filePath; NSString *_Nullable filePath = attachmentStream.originalFilePath;
XCTAssertNotNil(filePath); XCTAssertNotNil(filePath);
XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]); XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]);

@ -88,7 +88,7 @@ NS_ASSUME_NONNULL_BEGIN
TSAttachmentStream *attachmentStream; TSAttachmentStream *attachmentStream;
if ([attachment isKindOfClass:[TSAttachmentStream class]]) { if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
attachmentStream = (TSAttachmentStream *)attachment; attachmentStream = (TSAttachmentStream *)attachment;
thumbnailImage = attachmentStream.image; thumbnailImage = attachmentStream.originalImage;
} }
} else if (attachmentInfo.thumbnailAttachmentPointerId) { } else if (attachmentInfo.thumbnailAttachmentPointerId) {
// download failed, or hasn't completed yet. // download failed, or hasn't completed yet.
@ -179,7 +179,7 @@ NS_ASSUME_NONNULL_BEGIN
hasText = YES; hasText = YES;
quotedText = @""; quotedText = @"";
NSData *_Nullable oversizeTextData = [NSData dataWithContentsOfFile:attachmentStream.filePath]; NSData *_Nullable oversizeTextData = [NSData dataWithContentsOfFile:attachmentStream.originalFilePath];
if (oversizeTextData) { if (oversizeTextData) {
// We don't need to include the entire text body of the message, just // We don't need to include the entire text body of the message, just
// enough to render a snippet. kOversizeTextMessageSizeThreshold is our // enough to render a snippet. kOversizeTextMessageSizeThreshold is our
@ -227,7 +227,7 @@ NS_ASSUME_NONNULL_BEGIN
authorId:authorId authorId:authorId
body:quotedText body:quotedText
bodySource:TSQuotedMessageContentSourceLocal bodySource:TSQuotedMessageContentSourceLocal
thumbnailImage:quotedAttachment.thumbnailImage thumbnailImage:quotedAttachment.thumbnailImageSmallSync
contentType:quotedAttachment.contentType contentType:quotedAttachment.contentType
sourceFilename:quotedAttachment.sourceFilename sourceFilename:quotedAttachment.sourceFilename
attachmentStream:quotedAttachment attachmentStream:quotedAttachment

@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN
{ {
OWSAssert(stream); OWSAssert(stream);
[self showShareUIForURL:stream.mediaURL]; [self showShareUIForURL:stream.originalMediaURL];
} }
+ (void)showShareUIForURL:(NSURL *)url + (void)showShareUIForURL:(NSURL *)url

@ -191,7 +191,7 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
OWSAssert(attachmentStream); OWSAssert(attachmentStream);
OWSAssert(transaction); OWSAssert(transaction);
self.groupModel.groupImage = [attachmentStream image]; self.groupModel.groupImage = [attachmentStream thumbnailImageSmallSync];
[self saveWithTransaction:transaction]; [self saveWithTransaction:transaction];
[transaction addCompletionQueue:nil [transaction addCompletionQueue:nil

@ -0,0 +1,84 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import AVFoundation
public enum OWSMediaError: Error {
case failure(description: String)
}
@objc public class OWSMediaUtils: NSObject {
@available(*, unavailable, message:"do not instantiate this class.")
private override init() {
}
@objc public class func thumbnail(forImageAtPath path: String, maxDimension: CGFloat) throws -> UIImage {
guard FileManager.default.fileExists(atPath: path) else {
throw OWSMediaError.failure(description: "Media file missing.")
}
guard NSData.ows_isValidImage(atPath: path) else {
throw OWSMediaError.failure(description: "Invalid image.")
}
guard let originalImage = UIImage(contentsOfFile: path) else {
throw OWSMediaError.failure(description: "Could not load original image.")
}
guard let thumbnailImage = originalImage.resized(withMaxDimensionPoints: maxDimension) else {
throw OWSMediaError.failure(description: "Could not thumbnail image.")
}
return thumbnailImage
}
@objc public class func thumbnail(forVideoAtPath path: String, maxDimension: CGFloat) throws -> UIImage {
let maxSize = CGSize(width: maxDimension, height: maxDimension)
guard FileManager.default.fileExists(atPath: path) else {
throw OWSMediaError.failure(description: "Media file missing.")
}
let url = URL(fileURLWithPath: path)
let asset = AVURLAsset(url: url, options: nil)
guard isValidVideo(asset: asset) else {
throw OWSMediaError.failure(description: "Invalid video.")
}
let generator = AVAssetImageGenerator(asset: asset)
generator.maximumSize = maxSize
generator.appliesPreferredTrackTransform = true
let time: CMTime = CMTimeMake(1, 60)
let cgImage = try generator.copyCGImage(at: time, actualTime: nil)
let image = UIImage(cgImage: cgImage)
return image
}
@objc public class func isValidVideo(path: String) -> Bool {
guard FileManager.default.fileExists(atPath: path) else {
Logger.error("Media file missing.")
return false
}
let url = URL(fileURLWithPath: path)
let asset = AVURLAsset(url: url, options: nil)
return isValidVideo(asset: asset)
}
private class func isValidVideo(asset: AVURLAsset) -> Bool {
var maxTrackSize = CGSize.zero
for track: AVAssetTrack in asset.tracks(withMediaType: .video) {
let trackSize: CGSize = track.naturalSize
maxTrackSize.width = max(maxTrackSize.width, trackSize.width)
maxTrackSize.height = max(maxTrackSize.height, trackSize.height)
}
if maxTrackSize.width < 1.0 || maxTrackSize.height < 1.0 {
Logger.error("Invalid video size: \(maxTrackSize)")
return false
}
let kMaxValidSize: CGFloat = 3 * 1024.0
if maxTrackSize.width > kMaxValidSize || maxTrackSize.height > kMaxValidSize {
Logger.error("Invalid video dimensions: \(maxTrackSize)")
return false
}
return true
}
}

@ -0,0 +1,175 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import AVFoundation
public enum OWSThumbnailError: Error {
case failure(description: String)
case assertionFailure(description: String)
case externalError(description: String, underlyingError:Error)
}
@objc public class OWSLoadedThumbnail: NSObject {
public typealias DataSourceBlock = () throws -> Data
@objc
public let image: UIImage
let dataSourceBlock: DataSourceBlock
@objc
public init(image: UIImage, filePath: String) {
self.image = image
self.dataSourceBlock = {
return try Data(contentsOf: URL(fileURLWithPath: filePath))
}
}
@objc
public init(image: UIImage, data: Data) {
self.image = image
self.dataSourceBlock = {
return data
}
}
@objc
public func data() throws -> Data {
return try dataSourceBlock()
}
}
private struct OWSThumbnailRequest {
public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void
public typealias FailureBlock = (Error) -> Void
let attachment: TSAttachmentStream
let thumbnailDimensionPoints: UInt
let success: SuccessBlock
let failure: FailureBlock
init(attachment: TSAttachmentStream, thumbnailDimensionPoints: UInt, success: @escaping SuccessBlock, failure: @escaping FailureBlock) {
self.attachment = attachment
self.thumbnailDimensionPoints = thumbnailDimensionPoints
self.success = success
self.failure = failure
}
}
@objc public class OWSThumbnailService: NSObject {
// MARK: - Singleton class
@objc(shared)
public static let shared = OWSThumbnailService()
public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void
public typealias FailureBlock = (Error) -> Void
private let serialQueue = DispatchQueue(label: "OWSThumbnailService")
// 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.
private var thumbnailRequestStack = [OWSThumbnailRequest]()
private override init() {
super.init()
SwiftSingletons.register(self)
}
private func canThumbnailAttachment(attachment: TSAttachmentStream) -> Bool {
return attachment.isImage || attachment.isAnimated || attachment.isVideo
}
// completion will only be called on success.
// completion will be called async on the main thread.
@objc
public func ensureThumbnail(forAttachment attachment: TSAttachmentStream,
thumbnailDimensionPoints: UInt,
success: @escaping SuccessBlock,
failure: @escaping FailureBlock) {
serialQueue.async {
let thumbnailRequest = OWSThumbnailRequest(attachment: attachment, thumbnailDimensionPoints: thumbnailDimensionPoints, success: success, failure: failure)
self.thumbnailRequestStack.append(thumbnailRequest)
self.processNextRequestSync()
}
}
private func processNextRequestAsync() {
serialQueue.async {
self.processNextRequestSync()
}
}
// This should only be called on the serialQueue.
private func processNextRequestSync() {
guard let thumbnailRequest = thumbnailRequestStack.popLast() else {
return
}
do {
let loadedThumbnail = try process(thumbnailRequest: thumbnailRequest)
DispatchQueue.main.async {
thumbnailRequest.success(loadedThumbnail)
}
} catch {
Logger.error("Could not create thumbnail: \(error)")
DispatchQueue.main.async {
thumbnailRequest.failure(error)
}
}
}
// This should only be called on the serialQueue.
//
// 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.
private func process(thumbnailRequest: OWSThumbnailRequest) throws -> OWSLoadedThumbnail {
let attachment = thumbnailRequest.attachment
guard canThumbnailAttachment(attachment: attachment) else {
throw OWSThumbnailError.failure(description: "Cannot thumbnail attachment.")
}
let thumbnailPath = attachment.path(forThumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints)
if FileManager.default.fileExists(atPath: thumbnailPath) {
guard let image = UIImage(contentsOfFile: thumbnailPath) else {
throw OWSThumbnailError.failure(description: "Could not load thumbnail.")
}
return OWSLoadedThumbnail(image: image, filePath: thumbnailPath)
}
let thumbnailDirPath = (thumbnailPath as NSString).deletingLastPathComponent
guard OWSFileSystem.ensureDirectoryExists(thumbnailDirPath) else {
throw OWSThumbnailError.failure(description: "Could not create attachment's thumbnail directory.")
}
guard let originalFilePath = attachment.originalFilePath else {
throw OWSThumbnailError.failure(description: "Missing original file path.")
}
let maxDimension = CGFloat(thumbnailRequest.thumbnailDimensionPoints)
let thumbnailImage: UIImage
if attachment.isImage || attachment.isAnimated {
thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension)
} else if attachment.isVideo {
thumbnailImage = try OWSMediaUtils.thumbnail(forVideoAtPath: originalFilePath, maxDimension: maxDimension)
} else {
throw OWSThumbnailError.assertionFailure(description: "Invalid attachment type.")
}
guard let thumbnailData = UIImageJPEGRepresentation(thumbnailImage, 0.85) else {
throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
}
do {
try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath), options: .atomicWrite)
} catch let error as NSError {
throw OWSThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error)
}
return OWSLoadedThumbnail(image: thumbnailImage, data: thumbnailData)
}
}

@ -17,6 +17,9 @@ NS_ASSUME_NONNULL_BEGIN
@class TSAttachmentPointer; @class TSAttachmentPointer;
@class YapDatabaseReadWriteTransaction; @class YapDatabaseReadWriteTransaction;
typedef void (^OWSThumbnailSuccess)(UIImage *image);
typedef void (^OWSThumbnailFailure)(void);
@interface TSAttachmentStream : TSAttachment @interface TSAttachmentStream : TSAttachment
- (instancetype)init NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE;
@ -37,22 +40,21 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) NSDate *creationTimestamp; @property (nonatomic, readonly) NSDate *creationTimestamp;
#if TARGET_OS_IPHONE #if TARGET_OS_IPHONE
- (nullable UIImage *)image;
- (nullable UIImage *)thumbnailImage;
- (nullable NSData *)thumbnailData;
- (nullable NSData *)validStillImageData; - (nullable NSData *)validStillImageData;
#endif #endif
- (BOOL)isAnimated; @property (nonatomic, readonly) BOOL isAnimated;
- (BOOL)isImage; @property (nonatomic, readonly) BOOL isImage;
- (BOOL)isVideo; @property (nonatomic, readonly) BOOL isVideo;
- (BOOL)isAudio; @property (nonatomic, readonly) BOOL isAudio;
- (nullable NSURL *)mediaURL;
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType; @property (nonatomic, readonly, nullable) UIImage *originalImage;
@property (nonatomic, readonly, nullable) NSString *originalFilePath;
@property (nonatomic, readonly, nullable) NSURL *originalMediaURL;
- (nullable NSString *)filePath; - (NSArray<NSString *> *)allThumbnailPaths;
- (nullable NSString *)thumbnailPath;
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType;
- (nullable NSData *)readDataFromFileWithError:(NSError **)error; - (nullable NSData *)readDataFromFileWithError:(NSError **)error;
- (BOOL)writeData:(NSData *)data error:(NSError **)error; - (BOOL)writeData:(NSData *)data error:(NSError **)error;
@ -77,6 +79,24 @@ NS_ASSUME_NONNULL_BEGIN
// Non-nil for attachments which need "lazy backup restore." // Non-nil for attachments which need "lazy backup restore."
- (nullable OWSBackupFragment *)lazyRestoreFragment; - (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 success will be invoked if thumbnail can be generated;
// otherwise failure will be invoked.
//
// success and failure are invoked async on main.
- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint
success:(OWSThumbnailSuccess)success
failure:(OWSThumbnailFailure)failure;
- (nullable UIImage *)thumbnailImageSmallWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure;
- (nullable UIImage *)thumbnailImageMediumWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure;
- (nullable UIImage *)thumbnailImageLargeWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure;
- (nullable UIImage *)thumbnailImageSmallSync;
// This method should only be invoked by OWSThumbnailService.
- (NSString *)pathForThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints;
#pragma mark - Validation #pragma mark - Validation
- (BOOL)isValidImage; - (BOOL)isValidImage;

@ -8,13 +8,22 @@
#import "OWSFileSystem.h" #import "OWSFileSystem.h"
#import "TSAttachmentPointer.h" #import "TSAttachmentPointer.h"
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
#import <ImageIO/ImageIO.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h> #import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <YapDatabase/YapDatabase.h> #import <YapDatabase/YapDatabase.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
const CGFloat kMaxVideoStillSize = 1 * 1024; const NSUInteger kThumbnailDimensionPointsSmall = 200;
const NSUInteger kThumbnailDimensionPointsMedium = 450;
// This size is large enough to render full screen.
const NSUInteger ThumbnailDimensionPointsLarge()
{
CGSize screenSizePoints = UIScreen.mainScreen.bounds.size;
const CGFloat kMinZoomFactor = 2.f;
return MAX(screenSizePoints.width * kMinZoomFactor, screenSizePoints.height * kMinZoomFactor);
}
typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail);
@interface TSAttachmentStream () @interface TSAttachmentStream ()
@ -96,18 +105,9 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
_creationTimestamp = [NSDate new]; _creationTimestamp = [NSDate new];
} }
// This is going to be slow the first time it runs.
[self ensureThumbnail];
return self; return self;
} }
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[super saveWithTransaction:transaction];
[self ensureThumbnail];
}
- (void)upgradeFromAttachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion - (void)upgradeFromAttachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion
{ {
[super upgradeFromAttachmentSchemaVersion:attachmentSchemaVersion]; [super upgradeFromAttachmentSchemaVersion:attachmentSchemaVersion];
@ -156,7 +156,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
} }
self.localRelativeFilePath = localRelativeFilePath; self.localRelativeFilePath = localRelativeFilePath;
OWSAssert(self.filePath); OWSAssert(self.originalFilePath);
} }
#pragma mark - File Management #pragma mark - File Management
@ -164,7 +164,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
- (nullable NSData *)readDataFromFileWithError:(NSError **)error - (nullable NSData *)readDataFromFileWithError:(NSError **)error
{ {
*error = nil; *error = nil;
NSString *_Nullable filePath = self.filePath; NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) { if (!filePath) {
OWSFail(@"%@ Missing path for attachment.", self.logTag); OWSFail(@"%@ Missing path for attachment.", self.logTag);
return nil; return nil;
@ -177,7 +177,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
OWSAssert(data); OWSAssert(data);
*error = nil; *error = nil;
NSString *_Nullable filePath = self.filePath; NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) { if (!filePath) {
OWSFail(@"%@ Missing path for attachment.", self.logTag); OWSFail(@"%@ Missing path for attachment.", self.logTag);
return NO; return NO;
@ -190,7 +190,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
{ {
OWSAssert(dataSource); OWSAssert(dataSource);
NSString *_Nullable filePath = self.filePath; NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) { if (!filePath) {
OWSFail(@"%@ Missing path for attachment.", self.logTag); OWSFail(@"%@ Missing path for attachment.", self.logTag);
return NO; return NO;
@ -229,7 +229,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return attachmentsFolder; return attachmentsFolder;
} }
- (nullable NSString *)filePath - (nullable NSString *)originalFilePath
{ {
if (!self.localRelativeFilePath) { if (!self.localRelativeFilePath) {
OWSFail(@"%@ Attachment missing local file path.", self.logTag); OWSFail(@"%@ Attachment missing local file path.", self.logTag);
@ -239,9 +239,9 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return [[[self class] attachmentsFolder] stringByAppendingPathComponent:self.localRelativeFilePath]; return [[[self class] attachmentsFolder] stringByAppendingPathComponent:self.localRelativeFilePath];
} }
- (nullable NSString *)thumbnailPath - (nullable NSString *)legacyThumbnailPath
{ {
NSString *filePath = self.filePath; NSString *filePath = self.originalFilePath;
if (!filePath) { if (!filePath) {
OWSFail(@"%@ Attachment missing local file path.", self.logTag); OWSFail(@"%@ Attachment missing local file path.", self.logTag);
return nil; return nil;
@ -258,9 +258,28 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return [[containingDir stringByAppendingPathComponent:newFilename] stringByAppendingPathExtension:@"jpg"]; return [[containingDir stringByAppendingPathComponent:newFilename] stringByAppendingPathExtension:@"jpg"];
} }
- (nullable NSURL *)mediaURL - (NSString *)thumbnailsDirPath
{
if (!self.localRelativeFilePath) {
OWSFail(@"%@ Attachment missing local file path.", self.logTag);
return nil;
}
// Thumbnails are written to the caches directory, so that iOS can
// remove them if necessary.
NSString *dirName = [NSString stringWithFormat:@"%@-thumbnails", self.uniqueId];
return [OWSFileSystem.cachesDirectoryPath stringByAppendingPathComponent:dirName];
}
- (NSString *)pathForThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
{ {
NSString *_Nullable filePath = self.filePath; NSString *filename = [NSString stringWithFormat:@"thumbnail-%lu.jpg", (unsigned long)thumbnailDimensionPoints];
return [self.thumbnailsDirPath stringByAppendingPathComponent:filename];
}
- (nullable NSURL *)originalMediaURL
{
NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) { if (!filePath) {
OWSFail(@"%@ Missing path for attachment.", self.logTag); OWSFail(@"%@ Missing path for attachment.", self.logTag);
return nil; return nil;
@ -272,24 +291,31 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
{ {
NSError *error; NSError *error;
NSString *_Nullable thumbnailPath = self.thumbnailPath; NSString *thumbnailsDirPath = self.thumbnailsDirPath;
if (thumbnailPath) { if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailsDirPath]) {
[[NSFileManager defaultManager] removeItemAtPath:thumbnailPath error:&error]; BOOL success = [[NSFileManager defaultManager] removeItemAtPath:thumbnailsDirPath error:&error];
if (error || !success) {
DDLogError(@"%@ remove thumbnails dir failed with: %@", self.logTag, error);
}
}
if (error) { NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath;
DDLogError(@"%@ remove thumbnail errored with: %@", self.logTag, error); if (legacyThumbnailPath) {
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:legacyThumbnailPath error:&error];
if (error || !success) {
DDLogError(@"%@ remove legacy thumbnail failed with: %@", self.logTag, error);
} }
} }
NSString *_Nullable filePath = self.filePath; NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) { if (!filePath) {
OWSFail(@"%@ Missing path for attachment.", self.logTag); OWSFail(@"%@ Missing path for attachment.", self.logTag);
return; return;
} }
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (error || !success) {
if (error) { DDLogError(@"%@ remove file failed with: %@", self.logTag, error);
DDLogError(@"%@ remove file errored with: %@", self.logTag, error);
} }
} }
@ -321,31 +347,31 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
{ {
OWSAssert(self.isImage || self.isAnimated); OWSAssert(self.isImage || self.isAnimated);
return [NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType]; return [NSData ows_isValidImageAtPath:self.originalFilePath mimeType:self.contentType];
} }
- (BOOL)isValidVideo - (BOOL)isValidVideo
{ {
OWSAssert(self.isVideo); OWSAssert(self.isVideo);
return [NSData ows_isValidVideoAtURL:self.mediaURL]; return [OWSMediaUtils isValidVideoWithPath:self.originalFilePath];
} }
#pragma mark - #pragma mark -
- (nullable UIImage *)image - (nullable UIImage *)originalImage
{ {
if ([self isVideo]) { if ([self isVideo]) {
return [self videoStillImage]; return [self videoStillImage];
} else if ([self isImage] || [self isAnimated]) { } else if ([self isImage] || [self isAnimated]) {
NSURL *_Nullable mediaUrl = [self mediaURL]; NSURL *_Nullable mediaUrl = self.originalMediaURL;
if (!mediaUrl) { if (!mediaUrl) {
return nil; return nil;
} }
if (![self isValidImage]) { if (![self isValidImage]) {
return nil; return nil;
} }
return [[UIImage alloc] initWithContentsOfFile:self.filePath]; return [[UIImage alloc] initWithContentsOfFile:self.originalFilePath];
} else { } else {
return nil; return nil;
} }
@ -362,12 +388,12 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return nil; return nil;
} }
if (![NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType]) { if (![NSData ows_isValidImageAtPath:self.originalFilePath mimeType:self.contentType]) {
OWSFail(@"%@ skipping invalid image", self.logTag); OWSFail(@"%@ skipping invalid image", self.logTag);
return nil; return nil;
} }
return [NSData dataWithContentsOfFile:self.filePath]; return [NSData dataWithContentsOfFile:self.originalFilePath];
} }
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType + (BOOL)hasThumbnailForMimeType:(NSString *)contentType
@ -376,144 +402,17 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
[MIMETypeUtil isAnimated:contentType]); [MIMETypeUtil isAnimated:contentType]);
} }
- (nullable UIImage *)thumbnailImage
{
NSString *thumbnailPath = self.thumbnailPath;
if (!thumbnailPath) {
OWSAssert(!self.isImage && !self.isVideo && !self.isAnimated);
return nil;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) {
// This isn't true for some useful edge cases tested by the Debug UI.
DDLogError(@"%@ missing thumbnail for attachmentId: %@", self.logTag, self.uniqueId);
return nil;
}
return [UIImage imageWithContentsOfFile:self.thumbnailPath];
}
- (nullable NSData *)thumbnailData
{
NSString *thumbnailPath = self.thumbnailPath;
if (!thumbnailPath) {
OWSAssert(!self.isImage && !self.isVideo && !self.isAnimated);
return nil;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) {
OWSFail(@"%@ missing thumbnail for attachmentId: %@", self.logTag, self.uniqueId);
return nil;
}
return [NSData dataWithContentsOfFile:self.thumbnailPath];
}
- (void)ensureThumbnail
{
NSString *thumbnailPath = self.thumbnailPath;
if (!thumbnailPath) {
return;
}
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) {
// already exists
return;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:self.mediaURL.path]) {
DDLogError(@"%@ while generating thumbnail, source file doesn't exist: %@", self.logTag, self.mediaURL);
// If we're not lazy-restoring this message, the attachment should exist on disk.
OWSAssert(self.lazyRestoreFragmentId);
return;
}
// TODO proper resolution?
CGFloat thumbnailSize = 200;
UIImage *_Nullable result;
if (self.isImage || self.isAnimated) {
if (![self isValidImage]) {
DDLogWarn(@"%@ skipping thumbnail generation for invalid image at path: %@", self.logTag, self.filePath);
return;
}
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)self.mediaURL, NULL);
OWSAssert(imageSource != NULL);
NSDictionary *imageOptions = @{
(NSString const *)kCGImageSourceCreateThumbnailFromImageIfAbsent : (NSNumber const *)kCFBooleanTrue,
(NSString const *)kCGImageSourceThumbnailMaxPixelSize : @(thumbnailSize),
(NSString const *)kCGImageSourceCreateThumbnailWithTransform : (NSNumber const *)kCFBooleanTrue
};
CGImageRef thumbnail
= CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)imageOptions);
CFRelease(imageSource);
result = [[UIImage alloc] initWithCGImage:thumbnail];
CGImageRelease(thumbnail);
} else if (self.isVideo) {
if (![self isValidVideo]) {
DDLogWarn(@"%@ skipping thumbnail for invalid video at path: %@", self.logTag, self.filePath);
return;
}
result = [self videoStillImageWithMaxSize:CGSizeMake(thumbnailSize, thumbnailSize)];
} else {
OWSFail(@"%@ trying to generate thumnail for unexpected attachment: %@ of type: %@",
self.logTag,
self.uniqueId,
self.contentType);
}
if (result == nil) {
DDLogError(@"Unable to build thumbnail for attachmentId: %@", self.uniqueId);
return;
}
NSData *thumbnailData = UIImageJPEGRepresentation(result, 0.9);
OWSAssert(thumbnailData.length > 0);
DDLogDebug(@"%@ generated thumbnail with size: %lu", self.logTag, (unsigned long)thumbnailData.length);
[thumbnailData writeToFile:thumbnailPath atomically:YES];
}
- (nullable UIImage *)videoStillImage - (nullable UIImage *)videoStillImage
{ {
if (![self isValidVideo]) { NSError *error;
return nil; UIImage *_Nullable image = [OWSMediaUtils thumbnailForVideoAtPath:self.originalFilePath
} maxDimension:ThumbnailDimensionPointsLarge()
// Uses the assets intrinsic size by default error:&error];
return [self videoStillImageWithMaxSize:CGSizeMake(kMaxVideoStillSize, kMaxVideoStillSize)]; if (error || !image) {
} DDLogError(@"Could not create video still: %@.", error);
- (nullable UIImage *)videoStillImageWithMaxSize:(CGSize)maxSize
{
maxSize.width = MIN(maxSize.width, kMaxVideoStillSize);
maxSize.height = MIN(maxSize.height, kMaxVideoStillSize);
NSURL *_Nullable mediaUrl = [self mediaURL];
if (!mediaUrl) {
return nil;
}
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:mediaUrl options:nil];
AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
generator.maximumSize = maxSize;
generator.appliesPreferredTrackTransform = YES;
NSError *err = NULL;
CMTime time = CMTimeMake(1, 60);
CGImageRef imgRef = [generator copyCGImageAtTime:time actualTime:NULL error:&err];
if (imgRef == NULL) {
DDLogError(@"Could not generate video still: %@", self.filePath.pathExtension);
return nil; return nil;
} }
return image;
return [[UIImage alloc] initWithCGImage:imgRef];
} }
+ (void)deleteAttachments + (void)deleteAttachments
@ -546,7 +445,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
} }
return [self videoStillImage].size; return [self videoStillImage].size;
} else if ([self isImage] || [self isAnimated]) { } else if ([self isImage] || [self isAnimated]) {
NSURL *_Nullable mediaUrl = [self mediaURL]; NSURL *_Nullable mediaUrl = self.originalMediaURL;
if (!mediaUrl) { if (!mediaUrl) {
return CGSizeZero; return CGSizeZero;
} }
@ -656,7 +555,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
OWSAssert([self isAudio]); OWSAssert([self isAudio]);
NSError *error; NSError *error;
AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:self.mediaURL error:&error]; AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:self.originalMediaURL error:&error];
if (error && [error.domain isEqualToString:NSOSStatusErrorDomain] if (error && [error.domain isEqualToString:NSOSStatusErrorDomain]
&& (error.code == kAudioFileInvalidFileError || error.code == kAudioFileStreamError_InvalidFile)) { && (error.code == kAudioFileInvalidFileError || error.code == kAudioFileStreamError_InvalidFile)) {
// Ignore "invalid audio file" errors. // Ignore "invalid audio file" errors.
@ -665,7 +564,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
if (!error) { if (!error) {
return (CGFloat)[audioPlayer duration]; return (CGFloat)[audioPlayer duration];
} else { } else {
DDLogError(@"Could not find audio duration: %@", self.mediaURL); DDLogError(@"Could not find audio duration: %@", self.originalMediaURL);
return 0; return 0;
} }
} }
@ -727,6 +626,159 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return string; return string;
} }
#pragma mark - Thumbnails
- (nullable UIImage *)thumbnailImageWithSizeHint:(CGSize)sizeHint
success:(OWSThumbnailSuccess)success
failure:(OWSThumbnailFailure)failure
{
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 success:success failure:failure];
}
- (nullable UIImage *)thumbnailImageSmallWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure
{
return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall
success:success
failure:failure];
}
- (nullable UIImage *)thumbnailImageMediumWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure
{
return [self thumbnailImageWithThumbnailDimensionPoints:kThumbnailDimensionPointsMedium
success:success
failure:failure];
}
- (nullable UIImage *)thumbnailImageLargeWithSuccess:(OWSThumbnailSuccess)success failure:(OWSThumbnailFailure)failure
{
return [self thumbnailImageWithThumbnailDimensionPoints:ThumbnailDimensionPointsLarge()
success:success
failure:failure];
}
- (nullable UIImage *)thumbnailImageWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
success:(OWSThumbnailSuccess)success
failure:(OWSThumbnailFailure)failure
{
OWSLoadedThumbnail *_Nullable loadedThumbnail;
loadedThumbnail = [self loadedThumbnailWithThumbnailDimensionPoints:thumbnailDimensionPoints
success:^(OWSLoadedThumbnail *loadedThumbnail) {
success(loadedThumbnail.image);
}
failure:failure];
return loadedThumbnail.image;
}
- (nullable OWSLoadedThumbnail *)loadedThumbnailWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
success:(OWSLoadedThumbnailSuccess)success
failure:(OWSThumbnailFailure)failure
{
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 [[OWSLoadedThumbnail alloc] initWithImage:self.originalImage filePath:self.originalFilePath];
}
NSString *thumbnailPath = [self pathForThumbnailDimensionPoints:thumbnailDimensionPoints];
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) {
UIImage *_Nullable image = [UIImage imageWithContentsOfFile:thumbnailPath];
if (!image) {
OWSFail(@"couldn't load image.");
return nil;
}
return [[OWSLoadedThumbnail alloc] initWithImage:image filePath:thumbnailPath];
}
[OWSThumbnailService.shared ensureThumbnailForAttachment:self
thumbnailDimensionPoints:thumbnailDimensionPoints
success:success
failure:^(NSError *error) {
DDLogError(@"Failed to create thumbnail: %@", error);
failure();
}];
return nil;
}
- (nullable OWSLoadedThumbnail *)loadedThumbnailSmallSync
{
__block dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block OWSLoadedThumbnail *_Nullable loadedThumbnail = nil;
loadedThumbnail = [self loadedThumbnailWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall
success:^(OWSLoadedThumbnail *asyncLoadedThumbnail) {
@synchronized(self) {
loadedThumbnail = asyncLoadedThumbnail;
}
dispatch_semaphore_signal(semaphore);
}
failure:^{
dispatch_semaphore_signal(semaphore);
}];
// Wait up to N seconds.
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)));
@synchronized(self) {
return loadedThumbnail;
}
}
- (nullable UIImage *)thumbnailImageSmallSync
{
return [self loadedThumbnailSmallSync].image;
}
- (nullable NSData *)thumbnailDataSmallSync
{
NSError *error;
NSData *_Nullable data = [[self loadedThumbnailSmallSync] dataAndReturnError:&error];
if (error || !data) {
OWSFail(@"Couldn't load thumbnail data: %@", error);
return nil;
}
return data;
}
- (NSArray<NSString *> *)allThumbnailPaths
{
NSMutableArray<NSString *> *result = [NSMutableArray new];
NSString *thumbnailsDirPath = self.thumbnailsDirPath;
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailsDirPath]) {
NSError *error;
NSArray<NSString *> *_Nullable fileNames =
[[NSFileManager defaultManager] contentsOfDirectoryAtPath:thumbnailsDirPath error:&error];
if (error || !fileNames) {
OWSFail(@"contentsOfDirectoryAtPath failed with error: %@", error);
} else {
for (NSString *fileName in fileNames) {
NSString *filePath = [thumbnailsDirPath stringByAppendingPathComponent:fileName];
[result addObject:filePath];
}
}
}
NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath;
if (legacyThumbnailPath && [[NSFileManager defaultManager] fileExistsAtPath:legacyThumbnailPath]) {
[result addObject:legacyThumbnailPath];
}
return result;
}
#pragma mark - Update With... Methods #pragma mark - Update With... Methods
- (void)markForLazyRestoreWithFragment:(OWSBackupFragment *)lazyRestoreFragment - (void)markForLazyRestoreWithFragment:(OWSBackupFragment *)lazyRestoreFragment
@ -759,7 +811,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
- (nullable TSAttachmentStream *)cloneAsThumbnail - (nullable TSAttachmentStream *)cloneAsThumbnail
{ {
NSData *thumbnailData = self.thumbnailData; NSData *_Nullable thumbnailData = self.thumbnailDataSmallSync;
// Only some media types have thumbnails // Only some media types have thumbnails
if (!thumbnailData) { if (!thumbnailData) {
return nil; return nil;

@ -235,7 +235,7 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
[attachment isKindOfClass:TSAttachmentStream.class]) { [attachment isKindOfClass:TSAttachmentStream.class]) {
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
NSData *_Nullable data = [NSData dataWithContentsOfFile:attachmentStream.filePath]; NSData *_Nullable data = [NSData dataWithContentsOfFile:attachmentStream.originalFilePath];
if (data) { if (data) {
NSString *_Nullable text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSString *_Nullable text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (text) { if (text) {
@ -263,7 +263,7 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
// Handle oversize text attachments. // Handle oversize text attachments.
if ([attachment isKindOfClass:[TSAttachmentStream class]]) { if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment; TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
NSData *_Nullable data = [NSData dataWithContentsOfFile:attachmentStream.filePath]; NSData *_Nullable data = [NSData dataWithContentsOfFile:attachmentStream.originalFilePath];
if (data) { if (data) {
NSString *_Nullable text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSString *_Nullable text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (text) { if (text) {

@ -7,6 +7,7 @@
#import "NSData+Image.h" #import "NSData+Image.h"
#import "NSString+SSK.h" #import "NSString+SSK.h"
#import "OWSFileSystem.h" #import "OWSFileSystem.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@ -75,7 +76,7 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)isValidVideo - (BOOL)isValidVideo
{ {
return [NSData ows_isValidVideoAtURL:self.dataUrl]; return [OWSMediaUtils isValidVideoWithPath:self.dataUrl.path];
} }
- (void)setSourceFilename:(nullable NSString *)sourceFilename - (void)setSourceFilename:(nullable NSString *)sourceFilename

@ -11,6 +11,4 @@
- (BOOL)ows_isValidImage; - (BOOL)ows_isValidImage;
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType; - (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType;
+ (BOOL)ows_isValidVideoAtURL:(NSURL *)url;
@end @end

@ -132,12 +132,12 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
} }
// We only support (A)RGB and (A)Grayscale, so worst case is 4. // We only support (A)RGB and (A)Grayscale, so worst case is 4.
CGFloat kWorseCastComponentsPerPixel = 4; const CGFloat kWorseCastComponentsPerPixel = 4;
CGFloat bytesPerPixel = kWorseCastComponentsPerPixel * depthBytes; CGFloat bytesPerPixel = kWorseCastComponentsPerPixel * depthBytes;
CGFloat kMaxDimension = 2 * 1024; const CGFloat kExpectedBytePerPixel = 4;
CGFloat kExpectedBytePerPixel = 4; const CGFloat kMaxValidImageDimension = 4 * 1024;
CGFloat kMaxBytes = kMaxDimension * kMaxDimension * kExpectedBytePerPixel; CGFloat kMaxBytes = kMaxValidImageDimension * kMaxValidImageDimension * kExpectedBytePerPixel;
CGFloat actualBytes = width * height * bytesPerPixel; CGFloat actualBytes = width * height * bytesPerPixel;
if (actualBytes > kMaxBytes) { if (actualBytes > kMaxBytes) {
DDLogWarn(@"invalid dimensions width: %f, height %f, bytesPerPixel: %f", width, height, bytesPerPixel); DDLogWarn(@"invalid dimensions width: %f, height %f, bytesPerPixel: %f", width, height, bytesPerPixel);
@ -261,27 +261,4 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
return (width > 0 && width < kMaxValidSize && height > 0 && height < kMaxValidSize); return (width > 0 && width < kMaxValidSize && height > 0 && height < kMaxValidSize);
} }
+ (BOOL)ows_isValidVideoAtURL:(NSURL *)url
{
OWSAssert(url);
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
CGSize maxSize = CGSizeZero;
for (AVAssetTrack *track in [asset tracksWithMediaType:AVMediaTypeVideo]) {
CGSize trackSize = track.naturalSize;
maxSize.width = MAX(maxSize.width, trackSize.width);
maxSize.height = MAX(maxSize.height, trackSize.height);
}
if (maxSize.width < 1.f || maxSize.height < 1.f) {
DDLogError(@"Invalid video size: %@", NSStringFromCGSize(maxSize));
return NO;
}
const CGFloat kMaxSize = 3 * 1024.f;
if (maxSize.width > kMaxSize || maxSize.height > kMaxSize) {
DDLogError(@"Invalid video dimensions: %@", NSStringFromCGSize(maxSize));
return NO;
}
return YES;
}
@end @end

@ -1,5 +1,5 @@
// //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // Copyright (c) 2018 Open Whisper Systems. All rights reserved.
// //
import Foundation import Foundation

@ -9,7 +9,8 @@ NS_ASSUME_NONNULL_BEGIN
- (UIImage *)normalizedImage; - (UIImage *)normalizedImage;
- (UIImage *)resizedWithQuality:(CGInterpolationQuality)quality rate:(CGFloat)rate; - (UIImage *)resizedWithQuality:(CGInterpolationQuality)quality rate:(CGFloat)rate;
- (UIImage *)resizedImageToSize:(CGSize)dstSize; - (nullable UIImage *)resizedWithMaxDimensionPoints:(CGFloat)maxDimensionPoints;
- (nullable UIImage *)resizedImageToSize:(CGSize)dstSize;
- (UIImage *)resizedImageToFillPixelSize:(CGSize)boundingSize; - (UIImage *)resizedImageToFillPixelSize:(CGSize)boundingSize;
+ (UIImage *)imageWithColor:(UIColor *)color; + (UIImage *)imageWithColor:(UIColor *)color;

@ -35,9 +35,45 @@
return resized; return resized;
} }
- (nullable UIImage *)resizedWithMaxDimensionPoints:(CGFloat)maxDimensionPoints
{
CGSize originalSize = self.size;
if (originalSize.width < 1 || originalSize.height < 1) {
DDLogError(@"Invalid original size: %@", NSStringFromCGSize(originalSize));
return nil;
}
CGFloat maxOriginalDimensionPoints = MAX(originalSize.width, originalSize.height);
if (maxOriginalDimensionPoints < maxDimensionPoints) {
// Don't bother scaling an image that is already smaller than the max dimension.
return self;
}
CGSize thumbnailSize = CGSizeZero;
if (originalSize.width > originalSize.height) {
thumbnailSize.width = maxDimensionPoints;
thumbnailSize.height = round(maxDimensionPoints * originalSize.height / originalSize.width);
} else {
thumbnailSize.width = round(maxDimensionPoints * originalSize.width / originalSize.height);
thumbnailSize.height = maxDimensionPoints;
}
if (thumbnailSize.width < 1 || thumbnailSize.height < 1) {
DDLogError(@"Invalid thumbnail size: %@", NSStringFromCGSize(thumbnailSize));
return nil;
}
UIGraphicsBeginImageContext(CGSizeMake(thumbnailSize.width, thumbnailSize.height));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
[self drawInRect:CGRectMake(0, 0, thumbnailSize.width, thumbnailSize.height)];
UIImage *_Nullable resized = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resized;
}
// Source: https://github.com/AliSoftware/UIImage-Resize // Source: https://github.com/AliSoftware/UIImage-Resize
- (UIImage *)resizedImageToSize:(CGSize)dstSize - (nullable UIImage *)resizedImageToSize:(CGSize)dstSize
{ {
CGImageRef imgRef = self.CGImage; CGImageRef imgRef = self.CGImage;
// the below values are regardless of orientation : for UIImages from Camera, width>height (landscape) // the below values are regardless of orientation : for UIImages from Camera, width>height (landscape)
@ -106,10 +142,10 @@
UIGraphicsBeginImageContextWithOptions(dstSize, NO, self.scale); UIGraphicsBeginImageContextWithOptions(dstSize, NO, self.scale);
CGContextRef context = UIGraphicsGetCurrentContext(); CGContextRef context = UIGraphicsGetCurrentContext();
if (!context) { if (!context) {
return nil; return nil;
} }
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
if (orient == UIImageOrientationRight || orient == UIImageOrientationLeft) { if (orient == UIImageOrientationRight || orient == UIImageOrientationLeft) {
CGContextScaleCTM(context, -scaleRatio, scaleRatio); CGContextScaleCTM(context, -scaleRatio, scaleRatio);
@ -124,7 +160,7 @@
// we use srcSize (and not dstSize) as the size to specify is in user space (and we use the CTM to apply a // we use srcSize (and not dstSize) as the size to specify is in user space (and we use the CTM to apply a
// scaleRatio) // scaleRatio)
CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, srcSize.width, srcSize.height), imgRef); CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, srcSize.width, srcSize.height), imgRef);
UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext(); UIImage *_Nullable resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext(); UIGraphicsEndImageContext();
return resizedImage; return resizedImage;

Loading…
Cancel
Save