Merge remote-tracking branch 'origin/release/2.29.2'
This commit is contained in:
commit
f9eab5cd24
|
@ -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 = "<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>"; };
|
||||
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>"; };
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.29.1</string>
|
||||
<string>2.29.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
@ -38,7 +38,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2.29.1.1</string>
|
||||
<string>2.29.2.3</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LOGS_EMAIL</key>
|
||||
|
|
|
@ -192,7 +192,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
NSString *filename = self.attachmentStream.sourceFilename;
|
||||
if (!filename) {
|
||||
filename = [[self.attachmentStream filePath] lastPathComponent];
|
||||
filename = [self.attachmentStream.originalFilePath lastPathComponent];
|
||||
}
|
||||
NSString *topText = [[filename stringByDeletingPathExtension] ows_stripped];
|
||||
if (topText.length < 1) {
|
||||
|
|
|
@ -107,7 +107,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
NSString *filename = self.attachmentStream.sourceFilename;
|
||||
if (!filename) {
|
||||
filename = [[self.attachmentStream filePath] lastPathComponent];
|
||||
filename = [[self.attachmentStream originalFilePath] lastPathComponent];
|
||||
}
|
||||
NSString *fileExtension = filename.pathExtension;
|
||||
if (fileExtension.length < 1) {
|
||||
|
@ -149,7 +149,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
NSError *error;
|
||||
unsigned long long fileSize =
|
||||
[[NSFileManager defaultManager] attributesOfItemAtPath:[self.attachmentStream filePath] error:&error].fileSize;
|
||||
[[NSFileManager defaultManager] attributesOfItemAtPath:[self.attachmentStream originalFilePath] error:&error]
|
||||
.fileSize;
|
||||
OWSAssertDebug(!error);
|
||||
NSString *bottomText = [OWSFormat formatFileSize:fileSize];
|
||||
UILabel *bottomLabel = [UILabel new];
|
||||
|
|
|
@ -656,7 +656,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
- (nullable id)tryToLoadCellMedia:(nullable id (^)(void))loadCellMediaBlock
|
||||
mediaView:(UIView *)mediaView
|
||||
cacheKey:(NSString *)cacheKey
|
||||
shouldSkipCache:(BOOL)shouldSkipCache
|
||||
canLoadAsync:(BOOL)canLoadAsync
|
||||
{
|
||||
OWSAssertDebug(self.attachmentStream);
|
||||
OWSAssertDebug(mediaView);
|
||||
|
@ -675,11 +675,9 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
cellMedia = loadCellMediaBlock();
|
||||
if (cellMedia) {
|
||||
OWSLogVerbose(@"cell media cache miss");
|
||||
if (!shouldSkipCache) {
|
||||
[self.cellMediaCache setObject:cellMedia forKey:cacheKey];
|
||||
}
|
||||
} else {
|
||||
OWSLogError(@"Failed to load cell media: %@", [self.attachmentStream mediaURL]);
|
||||
[self.cellMediaCache setObject:cellMedia forKey:cacheKey];
|
||||
} else if (!canLoadAsync) {
|
||||
OWSLogError(@"Failed to load cell media: %@", self.attachmentStream.originalMediaURL);
|
||||
self.viewItem.didCellMediaFailToLoad = YES;
|
||||
[self showAttachmentErrorViewWithMediaView:mediaView];
|
||||
}
|
||||
|
@ -839,6 +837,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
[self addAttachmentUploadViewIfNecessary];
|
||||
|
||||
__weak OWSMessageBubbleView *weakSelf = self;
|
||||
__weak UIImageView *weakImageView = stillImageView;
|
||||
self.loadCellContentBlock = ^{
|
||||
OWSMessageBubbleView *strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
|
@ -848,19 +847,22 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
if (stillImageView.image) {
|
||||
return;
|
||||
}
|
||||
// Don't cache large still images.
|
||||
//
|
||||
// TODO: Don't use full size images in the message cells.
|
||||
const NSUInteger kMaxCachableSize = 1024 * 1024;
|
||||
BOOL shouldSkipCache =
|
||||
[OWSFileSystem fileSizeOfPath:strongSelf.attachmentStream.filePath].unsignedIntegerValue < kMaxCachableSize;
|
||||
stillImageView.image = [strongSelf tryToLoadCellMedia:^{
|
||||
OWSCAssertDebug([strongSelf.attachmentStream isImage]);
|
||||
return strongSelf.attachmentStream.image;
|
||||
}
|
||||
mediaView:stillImageView
|
||||
cacheKey:strongSelf.attachmentStream.uniqueId
|
||||
shouldSkipCache:shouldSkipCache];
|
||||
stillImageView.image = [strongSelf
|
||||
tryToLoadCellMedia:^{
|
||||
OWSCAssertDebug([strongSelf.attachmentStream isImage]);
|
||||
OWSCAssertDebug([strongSelf.attachmentStream isValidImage]);
|
||||
|
||||
return [strongSelf.attachmentStream
|
||||
thumbnailImageMediumWithSuccess:^(UIImage *image) {
|
||||
weakImageView.image = image;
|
||||
}
|
||||
failure:^{
|
||||
OWSLogError(@"Could not load thumbnail.");
|
||||
}];
|
||||
}
|
||||
mediaView:stillImageView
|
||||
cacheKey:strongSelf.attachmentStream.uniqueId
|
||||
canLoadAsync:YES];
|
||||
};
|
||||
self.unloadCellContentBlock = ^{
|
||||
OWSMessageBubbleView *strongSelf = weakSelf;
|
||||
|
@ -896,19 +898,21 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
if (animatedImageView.image) {
|
||||
return;
|
||||
}
|
||||
animatedImageView.image = [strongSelf tryToLoadCellMedia:^{
|
||||
OWSCAssertDebug([strongSelf.attachmentStream isAnimated]);
|
||||
animatedImageView.image = [strongSelf
|
||||
tryToLoadCellMedia:^{
|
||||
OWSCAssertDebug([strongSelf.attachmentStream isAnimated]);
|
||||
OWSCAssertDebug([strongSelf.attachmentStream isValidImage]);
|
||||
|
||||
NSString *_Nullable filePath = [strongSelf.attachmentStream filePath];
|
||||
YYImage *_Nullable animatedImage = nil;
|
||||
if (strongSelf.attachmentStream.isValidImage && filePath) {
|
||||
animatedImage = [YYImage imageWithContentsOfFile:filePath];
|
||||
NSString *_Nullable filePath = [strongSelf.attachmentStream originalFilePath];
|
||||
YYImage *_Nullable animatedImage = nil;
|
||||
if (strongSelf.attachmentStream.isValidImage && filePath) {
|
||||
animatedImage = [YYImage imageWithContentsOfFile:filePath];
|
||||
}
|
||||
return animatedImage;
|
||||
}
|
||||
return animatedImage;
|
||||
}
|
||||
mediaView:animatedImageView
|
||||
cacheKey:strongSelf.attachmentStream.uniqueId
|
||||
shouldSkipCache:NO];
|
||||
mediaView:animatedImageView
|
||||
cacheKey:strongSelf.attachmentStream.uniqueId
|
||||
canLoadAsync:NO];
|
||||
};
|
||||
self.unloadCellContentBlock = ^{
|
||||
OWSMessageBubbleView *strongSelf = weakSelf;
|
||||
|
@ -968,6 +972,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
}];
|
||||
|
||||
__weak OWSMessageBubbleView *weakSelf = self;
|
||||
__weak UIImageView *weakImageView = stillImageView;
|
||||
self.loadCellContentBlock = ^{
|
||||
OWSMessageBubbleView *strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
|
@ -977,14 +982,22 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
if (stillImageView.image) {
|
||||
return;
|
||||
}
|
||||
stillImageView.image = [strongSelf tryToLoadCellMedia:^{
|
||||
OWSCAssertDebug([strongSelf.attachmentStream isVideo]);
|
||||
stillImageView.image = [strongSelf
|
||||
tryToLoadCellMedia:^{
|
||||
OWSCAssertDebug([strongSelf.attachmentStream isVideo]);
|
||||
OWSCAssertDebug([strongSelf.attachmentStream isValidVideo]);
|
||||
|
||||
return strongSelf.attachmentStream.image;
|
||||
}
|
||||
mediaView:stillImageView
|
||||
cacheKey:strongSelf.attachmentStream.uniqueId
|
||||
shouldSkipCache:NO];
|
||||
return [strongSelf.attachmentStream
|
||||
thumbnailImageMediumWithSuccess:^(UIImage *image) {
|
||||
weakImageView.image = image;
|
||||
}
|
||||
failure:^{
|
||||
OWSLogError(@"Could not load thumbnail.");
|
||||
}];
|
||||
}
|
||||
mediaView:stillImageView
|
||||
cacheKey:strongSelf.attachmentStream.uniqueId
|
||||
canLoadAsync:YES];
|
||||
};
|
||||
self.unloadCellContentBlock = ^{
|
||||
OWSMessageBubbleView *strongSelf = weakSelf;
|
||||
|
|
|
@ -2224,8 +2224,8 @@ typedef enum : NSUInteger {
|
|||
OWSAssertDebug(attachmentStream);
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
if (![fileManager fileExistsAtPath:[attachmentStream.mediaURL path]]) {
|
||||
OWSFailDebug(@"Missing video file: %@", attachmentStream.mediaURL);
|
||||
if (![fileManager fileExistsAtPath:attachmentStream.originalFilePath]) {
|
||||
OWSFailDebug(@"Missing video file: %@", attachmentStream.originalMediaURL);
|
||||
}
|
||||
|
||||
[self dismissKeyBoard];
|
||||
|
@ -2240,7 +2240,8 @@ typedef enum : NSUInteger {
|
|||
[self.audioAttachmentPlayer stop];
|
||||
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.
|
||||
self.audioAttachmentPlayer.owner = viewItem;
|
||||
[self.audioAttachmentPlayer playWithPlaybackAudioCategory];
|
||||
|
|
|
@ -382,7 +382,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
return [self displayableTextForCacheKey:displayableTextCacheKey
|
||||
textBlock:^{
|
||||
NSData *textData = [NSData dataWithContentsOfURL:attachmentStream.mediaURL];
|
||||
NSData *textData =
|
||||
[NSData dataWithContentsOfURL:attachmentStream.originalMediaURL];
|
||||
NSString *text =
|
||||
[[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding];
|
||||
return text;
|
||||
|
@ -733,7 +734,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
OWSFailDebug(@"Unknown MIME type: %@", self.attachmentStream.contentType);
|
||||
utiType = (NSString *)kUTTypeGIF;
|
||||
}
|
||||
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream mediaURL]];
|
||||
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream originalMediaURL]];
|
||||
if (!data) {
|
||||
OWSFailDebug(@"Could not load attachment data");
|
||||
return;
|
||||
|
@ -814,7 +815,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
case OWSMessageCellType_Audio:
|
||||
return NO;
|
||||
case OWSMessageCellType_Video:
|
||||
return UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.mediaURL.path);
|
||||
return UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.originalFilePath);
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
return NO;
|
||||
case OWSMessageCellType_DownloadingAttachment: {
|
||||
|
@ -834,7 +835,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
break;
|
||||
case OWSMessageCellType_StillImage:
|
||||
case OWSMessageCellType_AnimatedImage: {
|
||||
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream mediaURL]];
|
||||
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream originalMediaURL]];
|
||||
if (!data) {
|
||||
OWSFailDebug(@"Could not load image data");
|
||||
return;
|
||||
|
@ -853,8 +854,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
OWSFailDebug(@"Cannot save media data.");
|
||||
break;
|
||||
case OWSMessageCellType_Video:
|
||||
if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.mediaURL.path)) {
|
||||
UISaveVideoAtPathToSavedPhotosAlbum(self.attachmentStream.mediaURL.path, self, nil, nil);
|
||||
if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.originalFilePath)) {
|
||||
UISaveVideoAtPathToSavedPhotosAlbum(self.attachmentStream.originalFilePath, self, nil, nil);
|
||||
} else {
|
||||
OWSFailDebug(@"Could not save incompatible video data.");
|
||||
}
|
||||
|
|
|
@ -387,7 +387,7 @@ class ConversationSearchViewController: UITableViewController {
|
|||
guard let strongSelf = self else { return }
|
||||
|
||||
guard let results = searchResults else {
|
||||
owsFailDebug("\(strongSelf.logTag) in \(#function) searchResults was unexpectedly nil")
|
||||
owsFailDebug("searchResults was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -35,11 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@property (nonatomic) UIView *replacingView;
|
||||
@property (nonatomic) UIButton *shareButton;
|
||||
|
||||
@property (nonatomic) NSData *fileData;
|
||||
|
||||
@property (nonatomic) TSAttachmentStream *attachmentStream;
|
||||
@property (nonatomic, nullable) ConversationViewItem *viewItem;
|
||||
@property (nonatomic, readonly) UIImage *image;
|
||||
@property (nonatomic, nullable) UIImage *image;
|
||||
|
||||
@property (nonatomic, nullable) OWSVideoPlayer *videoPlayer;
|
||||
@property (nonatomic, nullable) UIButton *playVideoButton;
|
||||
|
@ -55,6 +53,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation MediaDetailViewController
|
||||
|
||||
- (void)dealloc
|
||||
|
@ -72,8 +72,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
_galleryItemBox = galleryItemBox;
|
||||
_viewItem = viewItem;
|
||||
|
||||
// We cache the image data in case the attachment stream is deleted.
|
||||
_image = galleryItemBox.attachmentStream.image;
|
||||
__weak MediaDetailViewController *weakSelf = self;
|
||||
_image = [galleryItemBox.attachmentStream
|
||||
thumbnailImageLargeWithSuccess:^(UIImage *image) {
|
||||
weakSelf.image = image;
|
||||
[weakSelf updateContents];
|
||||
[weakSelf updateMinZoomScale];
|
||||
}
|
||||
failure:^{
|
||||
OWSLogWarn(@"Could not load media.");
|
||||
}];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
@ -83,22 +93,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return self.galleryItemBox.attachmentStream;
|
||||
}
|
||||
|
||||
- (NSURL *_Nullable)attachmentUrl
|
||||
{
|
||||
return self.attachmentStream.mediaURL;
|
||||
}
|
||||
|
||||
- (NSData *)fileData
|
||||
{
|
||||
if (!_fileData) {
|
||||
NSURL *_Nullable url = self.attachmentUrl;
|
||||
if (url) {
|
||||
_fileData = [NSData dataWithContentsOfURL:url];
|
||||
}
|
||||
}
|
||||
return _fileData;
|
||||
}
|
||||
|
||||
- (BOOL)isAnimated
|
||||
{
|
||||
return self.attachmentStream.isAnimated;
|
||||
|
@ -115,7 +109,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
self.view.backgroundColor = [UIColor clearColor];
|
||||
|
||||
[self createContents];
|
||||
[self updateContents];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
|
@ -134,6 +128,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (void)updateMinZoomScale
|
||||
{
|
||||
if (!self.image) {
|
||||
self.scrollView.minimumZoomScale = 1.f;
|
||||
self.scrollView.maximumZoomScale = 1.f;
|
||||
self.scrollView.zoomScale = 1.f;
|
||||
return;
|
||||
}
|
||||
|
||||
CGSize viewSize = self.scrollView.bounds.size;
|
||||
UIImage *image = self.image;
|
||||
OWSAssertDebug(image);
|
||||
|
@ -163,8 +164,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
#pragma mark - Initializers
|
||||
|
||||
- (void)createContents
|
||||
- (void)updateContents
|
||||
{
|
||||
[self.mediaView removeFromSuperview];
|
||||
[self.scrollView removeFromSuperview];
|
||||
[self.playVideoButton removeFromSuperview];
|
||||
[self.videoProgressBar removeFromSuperview];
|
||||
|
||||
UIScrollView *scrollView = [UIScrollView new];
|
||||
[self.view addSubview:scrollView];
|
||||
self.scrollView = scrollView;
|
||||
|
@ -184,19 +190,28 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
if (self.isAnimated) {
|
||||
if (self.attachmentStream.isValidImage) {
|
||||
YYImage *animatedGif = [YYImage imageWithData:self.fileData];
|
||||
YYImage *animatedGif = [YYImage imageWithContentsOfFile:self.attachmentStream.originalFilePath];
|
||||
YYAnimatedImageView *animatedView = [YYAnimatedImageView new];
|
||||
animatedView.image = animatedGif;
|
||||
self.mediaView = animatedView;
|
||||
} else {
|
||||
self.mediaView = [UIImageView new];
|
||||
self.mediaView = [UIView new];
|
||||
self.mediaView.backgroundColor = Theme.offBackgroundColor;
|
||||
}
|
||||
} else if (!self.image) {
|
||||
// Still loading thumbnail.
|
||||
self.mediaView = [UIView new];
|
||||
self.mediaView.backgroundColor = Theme.offBackgroundColor;
|
||||
} else if (self.isVideo) {
|
||||
self.mediaView = [self buildVideoPlayerView];
|
||||
if (self.attachmentStream.isValidVideo) {
|
||||
self.mediaView = [self buildVideoPlayerView];
|
||||
} else {
|
||||
self.mediaView = [UIView new];
|
||||
self.mediaView.backgroundColor = Theme.offBackgroundColor;
|
||||
}
|
||||
} else {
|
||||
// Present the static image using standard UIImageView
|
||||
UIImageView *imageView = [[UIImageView alloc] initWithImage:self.image];
|
||||
|
||||
self.mediaView = imageView;
|
||||
}
|
||||
|
||||
|
@ -260,12 +275,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (UIView *)buildVideoPlayerView
|
||||
{
|
||||
NSURL *_Nullable attachmentUrl = self.attachmentStream.originalMediaURL;
|
||||
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
if (![fileManager fileExistsAtPath:[self.attachmentUrl path]]) {
|
||||
if (![fileManager fileExistsAtPath:[attachmentUrl path]]) {
|
||||
OWSFailDebug(@"Missing video file");
|
||||
}
|
||||
|
||||
OWSVideoPlayer *player = [[OWSVideoPlayer alloc] initWithUrl:self.attachmentUrl];
|
||||
OWSVideoPlayer *player = [[OWSVideoPlayer alloc] initWithUrl:attachmentUrl];
|
||||
[player seekToTime:kCMTimeZero];
|
||||
player.delegate = self;
|
||||
self.videoPlayer = player;
|
||||
|
|
|
@ -8,9 +8,7 @@ public enum GalleryDirection {
|
|||
case before, after, around
|
||||
}
|
||||
|
||||
public struct MediaGalleryItem: Equatable, Hashable {
|
||||
let logTag = "[MediaGalleryItem]"
|
||||
|
||||
public class MediaGalleryItem: Equatable, Hashable {
|
||||
let message: TSMessage
|
||||
let attachmentStream: TSAttachmentStream
|
||||
let galleryDate: GalleryDate
|
||||
|
@ -22,33 +20,20 @@ public struct MediaGalleryItem: Equatable, Hashable {
|
|||
}
|
||||
|
||||
var isVideo: Bool {
|
||||
return attachmentStream.isVideo()
|
||||
return attachmentStream.isVideo
|
||||
}
|
||||
|
||||
var isAnimated: Bool {
|
||||
return attachmentStream.isAnimated()
|
||||
return attachmentStream.isAnimated
|
||||
}
|
||||
|
||||
var isImage: Bool {
|
||||
return attachmentStream.isImage()
|
||||
return attachmentStream.isImage
|
||||
}
|
||||
|
||||
var thumbnailImage: UIImage {
|
||||
guard let image = attachmentStream.thumbnailImage() else {
|
||||
owsFailDebug("unexpectedly unable to build attachment thumbnail")
|
||||
return UIImage()
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
var fullSizedImage: UIImage {
|
||||
guard let image = attachmentStream.image() else {
|
||||
owsFailDebug("unexpectedly unable to build attachment image")
|
||||
return UIImage()
|
||||
}
|
||||
|
||||
return image
|
||||
public typealias AsyncThumbnailBlock = (UIImage) -> Void
|
||||
func thumbnailImage(async:@escaping AsyncThumbnailBlock) -> UIImage? {
|
||||
return attachmentStream.thumbnailImageSmall(success: async, failure: {})
|
||||
}
|
||||
|
||||
// MARK: Equatable
|
||||
|
@ -308,7 +293,16 @@ class MediaGalleryViewController: OWSNavigationController, MediaGalleryDataSourc
|
|||
|
||||
// loadView hasn't necessarily been called yet.
|
||||
self.loadViewIfNeeded()
|
||||
self.presentationView.image = initialDetailItem.fullSizedImage
|
||||
|
||||
self.presentationView.image = initialDetailItem.attachmentStream.thumbnailImageLarge(success: { [weak self] (image) in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.presentationView.image = image
|
||||
}, failure: {
|
||||
Logger.warn("Could not load presentation image.")
|
||||
})
|
||||
|
||||
self.applyInitialMediaViewConstraints()
|
||||
|
||||
// Restore presentationView.alpha in case a previous dismiss left us in a bad state.
|
||||
|
@ -485,7 +479,14 @@ class MediaGalleryViewController: OWSNavigationController, MediaGalleryDataSourc
|
|||
// it sits on the screen in the conversation view.
|
||||
let changedItems = currentItem != self.initialDetailItem
|
||||
if changedItems {
|
||||
self.presentationView.image = currentItem.fullSizedImage
|
||||
self.presentationView.image = currentItem.attachmentStream.thumbnailImageLarge(success: { [weak self] (image) in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.presentationView.image = image
|
||||
}, failure: {
|
||||
Logger.warn("Could not load presentation image.")
|
||||
})
|
||||
self.applyOffscreenMediaViewConstraints()
|
||||
} else {
|
||||
self.applyInitialMediaViewConstraints()
|
||||
|
|
|
@ -922,7 +922,24 @@ private class MediaGalleryCell: UICollectionViewCell {
|
|||
|
||||
public func configure(item: MediaGalleryItem) {
|
||||
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 {
|
||||
self.contentTypeBadgeView.isHidden = false
|
||||
self.contentTypeBadgeView.image = MediaGalleryCell.videoBadgeImage
|
||||
|
|
|
@ -651,7 +651,7 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
|
|||
func didTapAudioViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let mediaURL = attachmentStream.mediaURL() else {
|
||||
guard let mediaURL = attachmentStream.originalMediaURL else {
|
||||
owsFailDebug("mediaURL was unexpectedly nil for attachment: \(attachmentStream)")
|
||||
return
|
||||
}
|
||||
|
|
|
@ -218,7 +218,7 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
|
|||
private let iceServers: [RTCIceServer]
|
||||
private let connectionConstraints: RTCMediaConstraints
|
||||
private let configuration: RTCConfiguration
|
||||
private let factory = RTCPeerConnectionFactory()
|
||||
private let factory: RTCPeerConnectionFactory
|
||||
|
||||
// DataChannel
|
||||
|
||||
|
@ -254,6 +254,12 @@ class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate, RTCDataChannelD
|
|||
self.iceServers = iceServers
|
||||
self.delegate = delegate
|
||||
|
||||
// Ensure we enable SW decoders to enable VP8 support
|
||||
let decoderFactory = RTCDefaultVideoDecoderFactory()
|
||||
let encoderFactory = RTCDefaultVideoEncoderFactory()
|
||||
let factory = RTCPeerConnectionFactory(encoderFactory: encoderFactory, decoderFactory: decoderFactory)
|
||||
|
||||
self.factory = factory
|
||||
configuration = RTCConfiguration()
|
||||
configuration.iceServers = iceServers
|
||||
configuration.bundlePolicy = .maxBundle
|
||||
|
|
|
@ -544,7 +544,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSAssertDebug(backupIO);
|
||||
OWSAssertDebug(completion);
|
||||
|
||||
NSString *_Nullable attachmentFilePath = [attachment filePath];
|
||||
NSString *_Nullable attachmentFilePath = [attachment originalFilePath];
|
||||
if (attachmentFilePath.length < 1) {
|
||||
OWSLogError(@"Attachment has invalid file path.");
|
||||
return completion(NO);
|
||||
|
@ -615,7 +615,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
NSString *_Nullable attachmentFilePath = [attachment filePath];
|
||||
NSString *_Nullable attachmentFilePath = [attachment originalFilePath];
|
||||
if (attachmentFilePath.length < 1) {
|
||||
OWSLogError(@"Attachment has invalid file path.");
|
||||
return completion(NO);
|
||||
|
|
|
@ -539,7 +539,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return NO;
|
||||
}
|
||||
TSAttachmentStream *attachmentStream = object;
|
||||
NSString *_Nullable filePath = attachmentStream.filePath;
|
||||
NSString *_Nullable filePath = attachmentStream.originalFilePath;
|
||||
if (!filePath) {
|
||||
OWSLogError(@"attachment is missing file.");
|
||||
return NO;
|
||||
|
|
|
@ -314,17 +314,15 @@ typedef void (^OrphanDataBlock)(OWSOrphanData *);
|
|||
|
||||
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
|
||||
attachmentStreamCount++;
|
||||
NSString *_Nullable filePath = [attachmentStream filePath];
|
||||
NSString *_Nullable filePath = [attachmentStream originalFilePath];
|
||||
if (filePath) {
|
||||
[allAttachmentFilePaths addObject:filePath];
|
||||
} else {
|
||||
OWSFailDebug(@"attachment has no file path.");
|
||||
}
|
||||
|
||||
NSString *_Nullable thumbnailPath = [attachmentStream thumbnailPath];
|
||||
if (thumbnailPath.length > 0) {
|
||||
[allAttachmentFilePaths addObject:thumbnailPath];
|
||||
}
|
||||
[allAttachmentFilePaths
|
||||
addObjectsFromArray:attachmentStream.allThumbnailPaths];
|
||||
}];
|
||||
|
||||
if (shouldAbort) {
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
|
||||
XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]);
|
||||
TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment;
|
||||
NSString *_Nullable filePath = attachmentStream.filePath;
|
||||
NSString *_Nullable filePath = attachmentStream.originalFilePath;
|
||||
XCTAssertNotNil(filePath);
|
||||
|
||||
XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]);
|
||||
|
@ -148,7 +148,7 @@
|
|||
TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
|
||||
XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]);
|
||||
TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment;
|
||||
NSString *_Nullable filePath = attachmentStream.filePath;
|
||||
NSString *_Nullable filePath = attachmentStream.originalFilePath;
|
||||
XCTAssertNotNil(filePath);
|
||||
|
||||
XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]);
|
||||
|
@ -170,7 +170,7 @@
|
|||
TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
|
||||
XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]);
|
||||
TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment;
|
||||
NSString *_Nullable filePath = attachmentStream.filePath;
|
||||
NSString *_Nullable filePath = attachmentStream.originalFilePath;
|
||||
XCTAssertNotNil(filePath);
|
||||
|
||||
XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]);
|
||||
|
@ -192,7 +192,7 @@
|
|||
TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
|
||||
XCTAssertTrue([attachment isKindOfClass:[TSAttachmentStream class]]);
|
||||
TSAttachmentStream *_Nullable attachmentStream = (TSAttachmentStream *)attachment;
|
||||
NSString *_Nullable filePath = attachmentStream.filePath;
|
||||
NSString *_Nullable filePath = attachmentStream.originalFilePath;
|
||||
XCTAssertNotNil(filePath);
|
||||
|
||||
XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]);
|
||||
|
|
|
@ -88,7 +88,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
TSAttachmentStream *attachmentStream;
|
||||
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
attachmentStream = (TSAttachmentStream *)attachment;
|
||||
thumbnailImage = attachmentStream.image;
|
||||
thumbnailImage = attachmentStream.thumbnailImageSmallSync;
|
||||
}
|
||||
} else if (attachmentInfo.thumbnailAttachmentPointerId) {
|
||||
// download failed, or hasn't completed yet.
|
||||
|
@ -179,7 +179,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
hasText = YES;
|
||||
quotedText = @"";
|
||||
|
||||
NSData *_Nullable oversizeTextData = [NSData dataWithContentsOfFile:attachmentStream.filePath];
|
||||
NSData *_Nullable oversizeTextData = [NSData dataWithContentsOfFile:attachmentStream.originalFilePath];
|
||||
if (oversizeTextData) {
|
||||
// We don't need to include the entire text body of the message, just
|
||||
// enough to render a snippet. kOversizeTextMessageSizeThreshold is our
|
||||
|
@ -227,7 +227,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
authorId:authorId
|
||||
body:quotedText
|
||||
bodySource:TSQuotedMessageContentSourceLocal
|
||||
thumbnailImage:quotedAttachment.thumbnailImage
|
||||
thumbnailImage:quotedAttachment.thumbnailImageSmallSync
|
||||
contentType:quotedAttachment.contentType
|
||||
sourceFilename:quotedAttachment.sourceFilename
|
||||
attachmentStream:quotedAttachment
|
||||
|
|
|
@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
{
|
||||
OWSAssertDebug(stream);
|
||||
|
||||
[self showShareUIForURL:stream.mediaURL];
|
||||
[self showShareUIForURL:stream.originalMediaURL];
|
||||
}
|
||||
|
||||
+ (void)showShareUIForURL:(NSURL *)url
|
||||
|
|
|
@ -180,16 +180,11 @@ public class SignalAttachment: NSObject {
|
|||
|
||||
// MARK: Constants
|
||||
|
||||
/**
|
||||
* Media Size constraints from Signal-Android
|
||||
*
|
||||
* https://github.com/signalapp/Signal-Android/blob/master/src/org/thoughtcrime/securesms/mms/PushMediaConstraints.java
|
||||
*/
|
||||
static let kMaxFileSizeAnimatedImage = UInt(25 * 1024 * 1024)
|
||||
static let kMaxFileSizeImage = UInt(6 * 1024 * 1024)
|
||||
static let kMaxFileSizeVideo = UInt(100 * 1024 * 1024)
|
||||
static let kMaxFileSizeAudio = UInt(100 * 1024 * 1024)
|
||||
static let kMaxFileSizeGeneric = UInt(100 * 1024 * 1024)
|
||||
static let kMaxFileSizeAnimatedImage = OWSMediaUtils.kMaxFileSizeAnimatedImage
|
||||
static let kMaxFileSizeImage = OWSMediaUtils.kMaxFileSizeImage
|
||||
static let kMaxFileSizeVideo = OWSMediaUtils.kMaxFileSizeVideo
|
||||
static let kMaxFileSizeAudio = OWSMediaUtils.kMaxFileSizeAudio
|
||||
static let kMaxFileSizeGeneric = OWSMediaUtils.kMaxFileSizeGeneric
|
||||
|
||||
// MARK: Constructor
|
||||
|
||||
|
|
|
@ -22,9 +22,6 @@ NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled";
|
|||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
#ifndef THEME_ENABLED
|
||||
return NO;
|
||||
#else
|
||||
if (!CurrentAppContext().isMainApp) {
|
||||
// Ignore theme in app extensions.
|
||||
return NO;
|
||||
|
@ -33,7 +30,6 @@ NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled";
|
|||
return [OWSPrimaryStorage.sharedManager.dbReadConnection boolForKey:ThemeKeyThemeEnabled
|
||||
inCollection:ThemeCollection
|
||||
defaultValue:NO];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (void)setIsDarkThemeEnabled:(BOOL)value
|
||||
|
|
|
@ -191,7 +191,7 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
OWSAssertDebug(attachmentStream);
|
||||
OWSAssertDebug(transaction);
|
||||
|
||||
self.groupModel.groupImage = [attachmentStream image];
|
||||
self.groupModel.groupImage = [attachmentStream thumbnailImageSmallSync];
|
||||
[self saveWithTransaction:transaction];
|
||||
|
||||
[transaction addCompletionQueue:nil
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
//
|
||||
// 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 {
|
||||
Logger.verbose("thumbnailing image: \(path)")
|
||||
|
||||
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 {
|
||||
Logger.verbose("thumbnailing video: \(path)")
|
||||
|
||||
guard isVideoOfValidContentTypeAndSize(path: path) else {
|
||||
throw OWSMediaError.failure(description: "Media file has missing or invalid length.")
|
||||
}
|
||||
|
||||
let maxSize = CGSize(width: maxDimension, height: maxDimension)
|
||||
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 isVideoOfValidContentTypeAndSize(path: path) else {
|
||||
Logger.error("Media file has missing or invalid length.")
|
||||
return false
|
||||
}
|
||||
|
||||
let url = URL(fileURLWithPath: path)
|
||||
let asset = AVURLAsset(url: url, options: nil)
|
||||
return isValidVideo(asset: asset)
|
||||
}
|
||||
|
||||
private class func isVideoOfValidContentTypeAndSize(path: String) -> Bool {
|
||||
guard FileManager.default.fileExists(atPath: path) else {
|
||||
Logger.error("Media file missing.")
|
||||
return false
|
||||
}
|
||||
let fileExtension = URL(fileURLWithPath: path).pathExtension
|
||||
guard let contentType = MIMETypeUtil.mimeType(forFileExtension: fileExtension) else {
|
||||
Logger.error("Media file has unknown content type.")
|
||||
return false
|
||||
}
|
||||
guard MIMETypeUtil.isSupportedVideoMIMEType(contentType) else {
|
||||
Logger.error("Media file has invalid content type.")
|
||||
return false
|
||||
}
|
||||
|
||||
guard let fileSize = OWSFileSystem.fileSize(ofPath: path) else {
|
||||
Logger.error("Media file has unknown length.")
|
||||
return false
|
||||
}
|
||||
return fileSize.uintValue <= kMaxFileSizeVideo
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if maxTrackSize.width > kMaxVideoDimensions || maxTrackSize.height > kMaxVideoDimensions {
|
||||
Logger.error("Invalid video dimensions: \(maxTrackSize)")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/**
|
||||
* Media Size constraints from Signal-Android
|
||||
*
|
||||
* https://github.com/signalapp/Signal-Android/blob/master/src/org/thoughtcrime/securesms/mms/PushMediaConstraints.java
|
||||
*/
|
||||
@objc
|
||||
public static let kMaxFileSizeAnimatedImage = UInt(25 * 1024 * 1024)
|
||||
@objc
|
||||
public static let kMaxFileSizeImage = UInt(6 * 1024 * 1024)
|
||||
@objc
|
||||
public static let kMaxFileSizeVideo = UInt(100 * 1024 * 1024)
|
||||
@objc
|
||||
public static let kMaxFileSizeAudio = UInt(100 * 1024 * 1024)
|
||||
@objc
|
||||
public static let kMaxFileSizeGeneric = UInt(100 * 1024 * 1024)
|
||||
|
||||
@objc
|
||||
public static let kMaxVideoDimensions: CGFloat = 3 * 1024
|
||||
@objc
|
||||
public static let kMaxAnimatedImageDimensions: UInt = 1 * 1024
|
||||
@objc
|
||||
public static let kMaxStillImageDimensions: UInt = 8 * 1024
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
// success and failure will be called async _off_ 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.global().async {
|
||||
thumbnailRequest.success(loadedThumbnail)
|
||||
}
|
||||
} catch {
|
||||
Logger.error("Could not create thumbnail: \(error)")
|
||||
|
||||
DispatchQueue.global().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)
|
||||
}
|
||||
|
||||
Logger.verbose("Creating thumbnail of size: \(thumbnailRequest.thumbnailDimensionPoints)")
|
||||
|
||||
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)
|
||||
}
|
||||
OWSFileSystem.protectFileOrFolder(atPath: thumbnailPath)
|
||||
return OWSLoadedThumbnail(image: thumbnailImage, data: thumbnailData)
|
||||
}
|
||||
}
|
|
@ -17,6 +17,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@class TSAttachmentPointer;
|
||||
@class YapDatabaseReadWriteTransaction;
|
||||
|
||||
typedef void (^OWSThumbnailSuccess)(UIImage *image);
|
||||
typedef void (^OWSThumbnailFailure)(void);
|
||||
|
||||
@interface TSAttachmentStream : TSAttachment
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
@ -37,23 +40,22 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@property (nonatomic, readonly) NSDate *creationTimestamp;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
- (nullable UIImage *)image;
|
||||
- (nullable UIImage *)thumbnailImage;
|
||||
- (nullable NSData *)thumbnailData;
|
||||
- (nullable NSData *)validStillImageData;
|
||||
#endif
|
||||
|
||||
- (BOOL)isAnimated;
|
||||
- (BOOL)isImage;
|
||||
- (BOOL)isVideo;
|
||||
- (BOOL)isAudio;
|
||||
- (nullable NSURL *)mediaURL;
|
||||
@property (nonatomic, readonly) BOOL isAnimated;
|
||||
@property (nonatomic, readonly) BOOL isImage;
|
||||
@property (nonatomic, readonly) BOOL isVideo;
|
||||
@property (nonatomic, readonly) BOOL isAudio;
|
||||
|
||||
@property (nonatomic, readonly, nullable) UIImage *originalImage;
|
||||
@property (nonatomic, readonly, nullable) NSString *originalFilePath;
|
||||
@property (nonatomic, readonly, nullable) NSURL *originalMediaURL;
|
||||
|
||||
- (NSArray<NSString *> *)allThumbnailPaths;
|
||||
|
||||
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType;
|
||||
|
||||
- (nullable NSString *)filePath;
|
||||
- (nullable NSString *)thumbnailPath;
|
||||
|
||||
- (nullable NSData *)readDataFromFileWithError:(NSError **)error;
|
||||
- (BOOL)writeData:(NSData *)data error:(NSError **)error;
|
||||
- (BOOL)writeDataSource:(DataSource *)dataSource;
|
||||
|
@ -77,6 +79,24 @@ 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 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
|
||||
|
||||
- (BOOL)isValidImage;
|
||||
|
|
|
@ -7,14 +7,24 @@
|
|||
#import "NSData+Image.h"
|
||||
#import "OWSFileSystem.h"
|
||||
#import "TSAttachmentPointer.h"
|
||||
#import "Threading.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
|
||||
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, screenSizePoints.height) * kMinZoomFactor;
|
||||
}
|
||||
|
||||
typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail);
|
||||
|
||||
@interface TSAttachmentStream ()
|
||||
|
||||
|
@ -32,6 +42,9 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
// Optional property. Only set for attachments which need "lazy backup restore."
|
||||
@property (nonatomic, nullable) NSString *lazyRestoreFragmentId;
|
||||
|
||||
@property (atomic, nullable) NSNumber *isValidImageCached;
|
||||
@property (atomic, nullable) NSNumber *isValidVideoCached;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
@ -96,18 +109,9 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
_creationTimestamp = [NSDate new];
|
||||
}
|
||||
|
||||
// This is going to be slow the first time it runs.
|
||||
[self ensureThumbnail];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
[super saveWithTransaction:transaction];
|
||||
[self ensureThumbnail];
|
||||
}
|
||||
|
||||
- (void)upgradeFromAttachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion
|
||||
{
|
||||
[super upgradeFromAttachmentSchemaVersion:attachmentSchemaVersion];
|
||||
|
@ -156,7 +160,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
}
|
||||
|
||||
self.localRelativeFilePath = localRelativeFilePath;
|
||||
OWSAssertDebug(self.filePath);
|
||||
OWSAssertDebug(self.originalFilePath);
|
||||
}
|
||||
|
||||
#pragma mark - File Management
|
||||
|
@ -164,7 +168,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
- (nullable NSData *)readDataFromFileWithError:(NSError **)error
|
||||
{
|
||||
*error = nil;
|
||||
NSString *_Nullable filePath = self.filePath;
|
||||
NSString *_Nullable filePath = self.originalFilePath;
|
||||
if (!filePath) {
|
||||
OWSFailDebug(@"Missing path for attachment.");
|
||||
return nil;
|
||||
|
@ -177,7 +181,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
OWSAssertDebug(data);
|
||||
|
||||
*error = nil;
|
||||
NSString *_Nullable filePath = self.filePath;
|
||||
NSString *_Nullable filePath = self.originalFilePath;
|
||||
if (!filePath) {
|
||||
OWSFailDebug(@"Missing path for attachment.");
|
||||
return NO;
|
||||
|
@ -190,7 +194,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
{
|
||||
OWSAssertDebug(dataSource);
|
||||
|
||||
NSString *_Nullable filePath = self.filePath;
|
||||
NSString *_Nullable filePath = self.originalFilePath;
|
||||
if (!filePath) {
|
||||
OWSFailDebug(@"Missing path for attachment.");
|
||||
return NO;
|
||||
|
@ -229,7 +233,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
return attachmentsFolder;
|
||||
}
|
||||
|
||||
- (nullable NSString *)filePath
|
||||
- (nullable NSString *)originalFilePath
|
||||
{
|
||||
if (!self.localRelativeFilePath) {
|
||||
OWSFailDebug(@"Attachment missing local file path.");
|
||||
|
@ -239,9 +243,9 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
return [[[self class] attachmentsFolder] stringByAppendingPathComponent:self.localRelativeFilePath];
|
||||
}
|
||||
|
||||
- (nullable NSString *)thumbnailPath
|
||||
- (nullable NSString *)legacyThumbnailPath
|
||||
{
|
||||
NSString *filePath = self.filePath;
|
||||
NSString *filePath = self.originalFilePath;
|
||||
if (!filePath) {
|
||||
OWSFailDebug(@"Attachment missing local file path.");
|
||||
return nil;
|
||||
|
@ -258,9 +262,28 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
return [[containingDir stringByAppendingPathComponent:newFilename] stringByAppendingPathExtension:@"jpg"];
|
||||
}
|
||||
|
||||
- (nullable NSURL *)mediaURL
|
||||
- (NSString *)thumbnailsDirPath
|
||||
{
|
||||
NSString *_Nullable filePath = self.filePath;
|
||||
if (!self.localRelativeFilePath) {
|
||||
OWSFailDebug(@"Attachment missing local file path.");
|
||||
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 *filename = [NSString stringWithFormat:@"thumbnail-%lu.jpg", (unsigned long)thumbnailDimensionPoints];
|
||||
return [self.thumbnailsDirPath stringByAppendingPathComponent:filename];
|
||||
}
|
||||
|
||||
- (nullable NSURL *)originalMediaURL
|
||||
{
|
||||
NSString *_Nullable filePath = self.originalFilePath;
|
||||
if (!filePath) {
|
||||
OWSFailDebug(@"Missing path for attachment.");
|
||||
return nil;
|
||||
|
@ -272,24 +295,31 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
{
|
||||
NSError *error;
|
||||
|
||||
NSString *_Nullable thumbnailPath = self.thumbnailPath;
|
||||
if (thumbnailPath) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:thumbnailPath error:&error];
|
||||
|
||||
if (error) {
|
||||
OWSLogError(@"remove thumbnail errored with: %@", error);
|
||||
NSString *thumbnailsDirPath = self.thumbnailsDirPath;
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailsDirPath]) {
|
||||
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:thumbnailsDirPath error:&error];
|
||||
if (error || !success) {
|
||||
OWSLogError(@"remove thumbnails dir failed with: %@", error);
|
||||
}
|
||||
}
|
||||
|
||||
NSString *_Nullable filePath = self.filePath;
|
||||
NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath;
|
||||
if (legacyThumbnailPath) {
|
||||
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:legacyThumbnailPath error:&error];
|
||||
|
||||
if (error || !success) {
|
||||
OWSLogError(@"remove legacy thumbnail failed with: %@", error);
|
||||
}
|
||||
}
|
||||
|
||||
NSString *_Nullable filePath = self.originalFilePath;
|
||||
if (!filePath) {
|
||||
OWSFailDebug(@"Missing path for attachment.");
|
||||
return;
|
||||
}
|
||||
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
|
||||
|
||||
if (error) {
|
||||
OWSLogError(@"remove file errored with: %@", error);
|
||||
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
|
||||
if (error || !success) {
|
||||
OWSLogError(@"remove file failed with: %@", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -321,31 +351,50 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
{
|
||||
OWSAssertDebug(self.isImage || self.isAnimated);
|
||||
|
||||
return [NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType];
|
||||
if (self.lazyRestoreFragment) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@synchronized(self) {
|
||||
if (!self.isValidImageCached) {
|
||||
self.isValidImageCached =
|
||||
@([NSData ows_isValidImageAtPath:self.originalFilePath mimeType:self.contentType]);
|
||||
}
|
||||
return self.isValidImageCached.boolValue;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isValidVideo
|
||||
{
|
||||
OWSAssertDebug(self.isVideo);
|
||||
|
||||
return [NSData ows_isValidVideoAtURL:self.mediaURL];
|
||||
if (self.lazyRestoreFragment) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@synchronized(self) {
|
||||
if (!self.isValidVideoCached) {
|
||||
self.isValidVideoCached = @([OWSMediaUtils isValidVideoWithPath:self.originalFilePath]);
|
||||
}
|
||||
return self.isValidVideoCached.boolValue;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (nullable UIImage *)image
|
||||
- (nullable UIImage *)originalImage
|
||||
{
|
||||
if ([self isVideo]) {
|
||||
return [self videoStillImage];
|
||||
} else if ([self isImage] || [self isAnimated]) {
|
||||
NSURL *_Nullable mediaUrl = [self mediaURL];
|
||||
NSURL *_Nullable mediaUrl = self.originalMediaURL;
|
||||
if (!mediaUrl) {
|
||||
return nil;
|
||||
}
|
||||
if (![self isValidImage]) {
|
||||
return nil;
|
||||
}
|
||||
return [[UIImage alloc] initWithContentsOfFile:self.filePath];
|
||||
return [[UIImage alloc] initWithContentsOfFile:self.originalFilePath];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
@ -362,12 +411,12 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
return nil;
|
||||
}
|
||||
|
||||
if (![NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType]) {
|
||||
OWSFailDebug(@"%@ skipping invalid image", self.logTag);
|
||||
if (![NSData ows_isValidImageAtPath:self.originalFilePath mimeType:self.contentType]) {
|
||||
OWSFailDebug(@"skipping invalid image");
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSData dataWithContentsOfFile:self.filePath];
|
||||
return [NSData dataWithContentsOfFile:self.originalFilePath];
|
||||
}
|
||||
|
||||
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType
|
||||
|
@ -376,142 +425,17 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
[MIMETypeUtil isAnimated:contentType]);
|
||||
}
|
||||
|
||||
- (nullable UIImage *)thumbnailImage
|
||||
{
|
||||
NSString *thumbnailPath = self.thumbnailPath;
|
||||
if (!thumbnailPath) {
|
||||
OWSAssertDebug(!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.
|
||||
OWSLogError(@"missing thumbnail for attachmentId: %@", self.uniqueId);
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [UIImage imageWithContentsOfFile:self.thumbnailPath];
|
||||
}
|
||||
|
||||
- (nullable NSData *)thumbnailData
|
||||
{
|
||||
NSString *thumbnailPath = self.thumbnailPath;
|
||||
if (!thumbnailPath) {
|
||||
OWSAssertDebug(!self.isImage && !self.isVideo && !self.isAnimated);
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) {
|
||||
OWSFailDebug(@"missing thumbnail for attachmentId: %@", 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]) {
|
||||
OWSLogError(@"while generating thumbnail, source file doesn't exist: %@", self.mediaURL);
|
||||
// If we're not lazy-restoring this message, the attachment should exist on disk.
|
||||
OWSAssertDebug(self.lazyRestoreFragmentId);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO proper resolution?
|
||||
CGFloat thumbnailSize = 200;
|
||||
|
||||
UIImage *_Nullable result;
|
||||
if (self.isImage || self.isAnimated) {
|
||||
if (![self isValidImage]) {
|
||||
OWSLogWarn(@"skipping thumbnail generation for invalid image at path: %@", self.filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)self.mediaURL, NULL);
|
||||
OWSAssertDebug(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]) {
|
||||
OWSLogWarn(@"Skipping thumbnail for invalid video at path: %@", self.filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
result = [self videoStillImageWithMaxSize:CGSizeMake(thumbnailSize, thumbnailSize)];
|
||||
} else {
|
||||
OWSFailDebug(
|
||||
@"trying to generate thumnail for unexpected attachment: %@ of type: %@", self.uniqueId, self.contentType);
|
||||
}
|
||||
|
||||
if (result == nil) {
|
||||
OWSLogError(@"Unable to build thumbnail for attachmentId: %@", self.uniqueId);
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *thumbnailData = UIImageJPEGRepresentation(result, 0.9);
|
||||
|
||||
OWSAssertDebug(thumbnailData.length > 0);
|
||||
OWSLogDebug(@"generated thumbnail with size: %lu", (unsigned long)thumbnailData.length);
|
||||
[thumbnailData writeToFile:thumbnailPath atomically:YES];
|
||||
}
|
||||
|
||||
- (nullable UIImage *)videoStillImage
|
||||
{
|
||||
if (![self isValidVideo]) {
|
||||
NSError *error;
|
||||
UIImage *_Nullable image = [OWSMediaUtils thumbnailForVideoAtPath:self.originalFilePath
|
||||
maxDimension:ThumbnailDimensionPointsLarge()
|
||||
error:&error];
|
||||
if (error || !image) {
|
||||
OWSLogError(@"Could not create video still: %@.", error);
|
||||
return nil;
|
||||
}
|
||||
// Uses the assets intrinsic size by default
|
||||
return [self videoStillImageWithMaxSize:CGSizeMake(kMaxVideoStillSize, kMaxVideoStillSize)];
|
||||
}
|
||||
|
||||
- (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) {
|
||||
OWSLogError(@"Could not generate video still: %@", self.filePath.pathExtension);
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[UIImage alloc] initWithCGImage:imgRef];
|
||||
return image;
|
||||
}
|
||||
|
||||
+ (void)deleteAttachments
|
||||
|
@ -544,7 +468,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
}
|
||||
return [self videoStillImage].size;
|
||||
} else if ([self isImage] || [self isAnimated]) {
|
||||
NSURL *_Nullable mediaUrl = [self mediaURL];
|
||||
NSURL *_Nullable mediaUrl = self.originalMediaURL;
|
||||
if (!mediaUrl) {
|
||||
return CGSizeZero;
|
||||
}
|
||||
|
@ -654,7 +578,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
OWSAssertDebug([self isAudio]);
|
||||
|
||||
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]
|
||||
&& (error.code == kAudioFileInvalidFileError || error.code == kAudioFileStreamError_InvalidFile)) {
|
||||
// Ignore "invalid audio file" errors.
|
||||
|
@ -663,7 +587,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
if (!error) {
|
||||
return (CGFloat)[audioPlayer duration];
|
||||
} else {
|
||||
OWSLogError(@"Could not find audio duration: %@", self.mediaURL);
|
||||
OWSLogError(@"Could not find audio duration: %@", self.originalMediaURL);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -725,6 +649,180 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
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) {
|
||||
DispatchMainThreadSafe(^{
|
||||
success(loadedThumbnail.image);
|
||||
});
|
||||
}
|
||||
failure:^{
|
||||
DispatchMainThreadSafe(^{
|
||||
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) {
|
||||
OWSFailDebug(@"couldn't load image.");
|
||||
return nil;
|
||||
}
|
||||
return [[OWSLoadedThumbnail alloc] initWithImage:image filePath:thumbnailPath];
|
||||
}
|
||||
|
||||
[OWSThumbnailService.shared ensureThumbnailForAttachment:self
|
||||
thumbnailDimensionPoints:thumbnailDimensionPoints
|
||||
success:success
|
||||
failure:^(NSError *error) {
|
||||
OWSLogError(@"Failed to create thumbnail: %@", error);
|
||||
failure();
|
||||
}];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (nullable OWSLoadedThumbnail *)loadedThumbnailSmallSync
|
||||
{
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
|
||||
__block OWSLoadedThumbnail *_Nullable asyncLoadedThumbnail = nil;
|
||||
OWSLoadedThumbnail *_Nullable syncLoadedThumbnail = nil;
|
||||
syncLoadedThumbnail = [self loadedThumbnailWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall
|
||||
success:^(OWSLoadedThumbnail *asyncLoadedThumbnail) {
|
||||
@synchronized(self) {
|
||||
asyncLoadedThumbnail = asyncLoadedThumbnail;
|
||||
}
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}
|
||||
failure:^{
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}];
|
||||
|
||||
if (syncLoadedThumbnail) {
|
||||
return syncLoadedThumbnail;
|
||||
}
|
||||
|
||||
// Wait up to N seconds.
|
||||
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)));
|
||||
@synchronized(self) {
|
||||
return asyncLoadedThumbnail;
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable UIImage *)thumbnailImageSmallSync
|
||||
{
|
||||
OWSLoadedThumbnail *_Nullable loadedThumbnail = [self loadedThumbnailSmallSync];
|
||||
if (!loadedThumbnail) {
|
||||
OWSLogInfo(@"Couldn't load small thumbnail sync.");
|
||||
return nil;
|
||||
}
|
||||
return loadedThumbnail.image;
|
||||
}
|
||||
|
||||
- (nullable NSData *)thumbnailDataSmallSync
|
||||
{
|
||||
OWSLoadedThumbnail *_Nullable loadedThumbnail = [self loadedThumbnailSmallSync];
|
||||
if (!loadedThumbnail) {
|
||||
OWSLogInfo(@"Couldn't load small thumbnail sync.");
|
||||
return nil;
|
||||
}
|
||||
NSError *error;
|
||||
NSData *_Nullable data = [loadedThumbnail dataAndReturnError:&error];
|
||||
if (error || !data) {
|
||||
OWSFailDebug(@"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) {
|
||||
OWSFailDebug(@"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
|
||||
|
||||
- (void)markForLazyRestoreWithFragment:(OWSBackupFragment *)lazyRestoreFragment
|
||||
|
@ -757,7 +855,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
|
|||
|
||||
- (nullable TSAttachmentStream *)cloneAsThumbnail
|
||||
{
|
||||
NSData *thumbnailData = self.thumbnailData;
|
||||
NSData *_Nullable thumbnailData = self.thumbnailDataSmallSync;
|
||||
// Only some media types have thumbnails
|
||||
if (!thumbnailData) {
|
||||
return nil;
|
||||
|
|
|
@ -232,7 +232,7 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
|
|||
[attachment isKindOfClass:TSAttachmentStream.class]) {
|
||||
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
|
||||
|
||||
NSData *_Nullable data = [NSData dataWithContentsOfFile:attachmentStream.filePath];
|
||||
NSData *_Nullable data = [NSData dataWithContentsOfFile:attachmentStream.originalFilePath];
|
||||
if (data) {
|
||||
NSString *_Nullable text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
if (text) {
|
||||
|
@ -260,7 +260,7 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
|
|||
// Handle oversize text attachments.
|
||||
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
|
||||
NSData *_Nullable data = [NSData dataWithContentsOfFile:attachmentStream.filePath];
|
||||
NSData *_Nullable data = [NSData dataWithContentsOfFile:attachmentStream.originalFilePath];
|
||||
if (data) {
|
||||
NSString *_Nullable text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
if (text) {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#import "NSData+Image.h"
|
||||
#import "NSString+SSK.h"
|
||||
#import "OWSFileSystem.h"
|
||||
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -75,7 +76,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (BOOL)isValidVideo
|
||||
{
|
||||
return [NSData ows_isValidVideoAtURL:self.dataUrl];
|
||||
return [OWSMediaUtils isValidVideoWithPath:self.dataUrl.path];
|
||||
}
|
||||
|
||||
- (void)setSourceFilename:(nullable NSString *)sourceFilename
|
||||
|
|
|
@ -11,6 +11,4 @@
|
|||
- (BOOL)ows_isValidImage;
|
||||
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType;
|
||||
|
||||
+ (BOOL)ows_isValidVideoAtURL:(NSURL *)url;
|
||||
|
||||
@end
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSData+Image.h"
|
||||
#import "MIMETypeUtil.h"
|
||||
#import "NSData+Image.h"
|
||||
#import "OWSFileSystem.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, ImageFormat) {
|
||||
ImageFormat_Unknown,
|
||||
|
@ -24,11 +26,23 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
|
|||
|
||||
- (BOOL)ows_isValidImage
|
||||
{
|
||||
if (![self ows_isValidImageWithMimeType:nil]) {
|
||||
ImageFormat imageFormat = [self ows_guessImageFormat];
|
||||
|
||||
BOOL isAnimated = imageFormat == ImageFormat_Gif;
|
||||
|
||||
const NSUInteger kMaxFileSize
|
||||
= (isAnimated ? OWSMediaUtils.kMaxFileSizeAnimatedImage : OWSMediaUtils.kMaxFileSizeImage);
|
||||
NSUInteger fileSize = self.length;
|
||||
if (fileSize > kMaxFileSize) {
|
||||
OWSLogWarn(@"Oversize image.");
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (![self ows_hasValidImageDimensions]) {
|
||||
if (![self ows_isValidImageWithMimeType:nil imageFormat:imageFormat]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (![self ows_hasValidImageDimensionsWithIsAnimated:isAnimated]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
@ -37,6 +51,36 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
|
|||
|
||||
+ (BOOL)ows_isValidImageAtPath:(NSString *)filePath mimeType:(nullable NSString *)mimeType
|
||||
{
|
||||
if (mimeType.length < 1) {
|
||||
NSString *fileExtension = [filePath pathExtension].lowercaseString;
|
||||
mimeType = [MIMETypeUtil mimeTypeForFileExtension:fileExtension];
|
||||
}
|
||||
if (mimeType.length < 1) {
|
||||
OWSLogError(@"Image has unknown MIME type.");
|
||||
return NO;
|
||||
}
|
||||
NSNumber *_Nullable fileSize = [OWSFileSystem fileSizeOfPath:filePath];
|
||||
if (!fileSize) {
|
||||
OWSLogError(@"Could not determine file size.");
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL isAnimated = [MIMETypeUtil isSupportedAnimatedMIMEType:mimeType];
|
||||
if (isAnimated) {
|
||||
if (fileSize.unsignedIntegerValue > OWSMediaUtils.kMaxFileSizeAnimatedImage) {
|
||||
OWSLogWarn(@"Oversize animated image.");
|
||||
return NO;
|
||||
}
|
||||
} else if ([MIMETypeUtil isSupportedImageMIMEType:mimeType]) {
|
||||
if (fileSize.unsignedIntegerValue > OWSMediaUtils.kMaxFileSizeImage) {
|
||||
OWSLogWarn(@"Oversize still image.");
|
||||
return NO;
|
||||
}
|
||||
} else {
|
||||
OWSLogError(@"Image has unsupported MIME type.");
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
NSData *_Nullable data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];
|
||||
if (!data || error) {
|
||||
|
@ -48,26 +92,26 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
|
|||
return NO;
|
||||
}
|
||||
|
||||
if (![self ows_hasValidImageDimensionsAtPath:filePath]) {
|
||||
OWSLogError(@"image had invalid dimensions.");
|
||||
if (![self ows_hasValidImageDimensionsAtPath:filePath isAnimated:isAnimated]) {
|
||||
OWSLogError(@"%@ image had invalid dimensions.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)ows_hasValidImageDimensions
|
||||
- (BOOL)ows_hasValidImageDimensionsWithIsAnimated:(BOOL)isAnimated
|
||||
{
|
||||
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self, NULL);
|
||||
if (imageSource == NULL) {
|
||||
return NO;
|
||||
}
|
||||
BOOL result = [NSData ows_hasValidImageDimensionWithImageSource:imageSource];
|
||||
BOOL result = [NSData ows_hasValidImageDimensionWithImageSource:imageSource isAnimated:isAnimated];
|
||||
CFRelease(imageSource);
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (BOOL)ows_hasValidImageDimensionsAtPath:(NSString *)path
|
||||
+ (BOOL)ows_hasValidImageDimensionsAtPath:(NSString *)path isAnimated:(BOOL)isAnimated
|
||||
{
|
||||
NSURL *url = [NSURL fileURLWithPath:path];
|
||||
if (!url) {
|
||||
|
@ -78,12 +122,12 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
|
|||
if (imageSource == NULL) {
|
||||
return NO;
|
||||
}
|
||||
BOOL result = [self ows_hasValidImageDimensionWithImageSource:imageSource];
|
||||
BOOL result = [self ows_hasValidImageDimensionWithImageSource:imageSource isAnimated:isAnimated];
|
||||
CFRelease(imageSource);
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (BOOL)ows_hasValidImageDimensionWithImageSource:(CGImageSourceRef)imageSource
|
||||
+ (BOOL)ows_hasValidImageDimensionWithImageSource:(CGImageSourceRef)imageSource isAnimated:(BOOL)isAnimated
|
||||
{
|
||||
OWSAssertDebug(imageSource);
|
||||
|
||||
|
@ -116,6 +160,7 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
|
|||
return NO;
|
||||
}
|
||||
NSUInteger depthBits = depthNumber.unsignedIntegerValue;
|
||||
// This should usually be 1.
|
||||
CGFloat depthBytes = (CGFloat)ceil(depthBits / 8.f);
|
||||
|
||||
/* The color model of the image such as "RGB", "CMYK", "Gray", or "Lab".
|
||||
|
@ -132,12 +177,13 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
|
|||
}
|
||||
|
||||
// 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 kMaxDimension = 2 * 1024;
|
||||
CGFloat kExpectedBytePerPixel = 4;
|
||||
CGFloat kMaxBytes = kMaxDimension * kMaxDimension * kExpectedBytePerPixel;
|
||||
const CGFloat kExpectedBytePerPixel = 4;
|
||||
CGFloat kMaxValidImageDimension
|
||||
= (isAnimated ? OWSMediaUtils.kMaxAnimatedImageDimensions : OWSMediaUtils.kMaxStillImageDimensions);
|
||||
CGFloat kMaxBytes = kMaxValidImageDimension * kMaxValidImageDimension * kExpectedBytePerPixel;
|
||||
CGFloat actualBytes = width * height * bytesPerPixel;
|
||||
if (actualBytes > kMaxBytes) {
|
||||
OWSLogWarn(@"invalid dimensions width: %f, height %f, bytesPerPixel: %f", width, height, bytesPerPixel);
|
||||
|
@ -148,6 +194,12 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
|
|||
}
|
||||
|
||||
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType
|
||||
{
|
||||
ImageFormat imageFormat = [self ows_guessImageFormat];
|
||||
return [self ows_isValidImageWithMimeType:mimeType imageFormat:imageFormat];
|
||||
}
|
||||
|
||||
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType imageFormat:(ImageFormat)imageFormat
|
||||
{
|
||||
// Don't trust the file extension; iOS (e.g. UIKit, Core Graphics) will happily
|
||||
// load a .gif with a .png file extension.
|
||||
|
@ -156,7 +208,6 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
|
|||
//
|
||||
// If the image has a declared MIME type, ensure that agrees with the
|
||||
// deduced image format.
|
||||
ImageFormat imageFormat = [self ows_guessImageFormat];
|
||||
switch (imageFormat) {
|
||||
case ImageFormat_Unknown:
|
||||
return NO;
|
||||
|
@ -261,27 +312,4 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
|
|||
return (width > 0 && width < kMaxValidSize && height > 0 && height < kMaxValidSize);
|
||||
}
|
||||
|
||||
+ (BOOL)ows_isValidVideoAtURL:(NSURL *)url
|
||||
{
|
||||
OWSAssertDebug(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) {
|
||||
OWSLogError(@"Invalid video size: %@", NSStringFromCGSize(maxSize));
|
||||
return NO;
|
||||
}
|
||||
const CGFloat kMaxSize = 3 * 1024.f;
|
||||
if (maxSize.width > kMaxSize || maxSize.height > kMaxSize) {
|
||||
OWSLogError(@"Invalid video dimensions: %@", NSStringFromCGSize(maxSize));
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -9,7 +9,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (UIImage *)normalizedImage;
|
||||
- (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 *)imageWithColor:(UIColor *)color;
|
||||
|
|
|
@ -35,9 +35,49 @@
|
|||
return resized;
|
||||
}
|
||||
|
||||
- (nullable UIImage *)resizedWithMaxDimensionPoints:(CGFloat)maxDimensionPoints
|
||||
{
|
||||
CGSize originalSize = self.size;
|
||||
if (originalSize.width < 1 || originalSize.height < 1) {
|
||||
OWSLogError(@"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) {
|
||||
OWSLogError(@"Invalid thumbnail size: %@", NSStringFromCGSize(thumbnailSize));
|
||||
return nil;
|
||||
}
|
||||
|
||||
UIGraphicsBeginImageContext(CGSizeMake(thumbnailSize.width, thumbnailSize.height));
|
||||
CGContextRef _Nullable context = UIGraphicsGetCurrentContext();
|
||||
if (context == NULL) {
|
||||
OWSLogError(@"Couldn't create context.");
|
||||
return nil;
|
||||
}
|
||||
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
|
||||
|
||||
- (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 +146,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 +164,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;
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.29.1</string>
|
||||
<string>2.29.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2.29.1.1</string>
|
||||
<string>2.29.2.3</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
|
Loading…
Reference in New Issue