Merge remote-tracking branch 'origin/release/2.29.2'

This commit is contained in:
Matthew Chen 2018-09-07 15:27:50 -04:00
commit f9eab5cd24
34 changed files with 917 additions and 377 deletions

View File

@ -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 */,

View File

@ -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>

View File

@ -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) {

View File

@ -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];

View File

@ -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;

View File

@ -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];

View File

@ -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.");
}

View File

@ -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
}

View File

@ -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;

View File

@ -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()

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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) {

View File

@ -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]);

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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

View File

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

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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>