Merge branch 'charlesmchen/convoThumbnails' into release/2.29.2

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

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

@ -148,7 +148,6 @@
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; };
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 */,

@ -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;
OWSAssert(!error);
NSString *bottomText = [OWSFormat formatFileSize:fileSize];
UILabel *bottomLabel = [UILabel new];

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

@ -2219,8 +2219,8 @@ typedef enum : NSUInteger {
OWSAssert(attachmentStream);
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:[attachmentStream.mediaURL path]]) {
OWSFail(@"%@ Missing video file: %@", self.logTag, attachmentStream.mediaURL);
if (![fileManager fileExistsAtPath:attachmentStream.originalFilePath]) {
OWSFail(@"%@ Missing video file: %@", self.logTag, attachmentStream.originalFilePath);
}
[self dismissKeyBoard];
@ -2235,7 +2235,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)
OWSFail(@"%@ Unknown MIME type: %@", self.logTag, self.attachmentStream.contentType);
utiType = (NSString *)kUTTypeGIF;
}
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream mediaURL]];
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream originalMediaURL]];
if (!data) {
OWSFail(@"%@ Could not load attachment data", self.logTag);
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) {
OWSFail(@"%@ Could not load image data", self.logTag);
return;
@ -853,8 +854,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
OWSFail(@"%@ Cannot save media data.", self.logTag);
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 {
OWSFail(@"%@ Could not save incompatible video data.", self.logTag);
}

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

@ -22,28 +22,24 @@ 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 {
owsFail("\(logTag) in \(#function) unexpectedly unable to build attachment thumbnail")
return UIImage()
}
return image
public typealias AsyncThumbnailBlock = (UIImage) -> Void
func thumbnailImage(async:@escaping AsyncThumbnailBlock) -> UIImage? {
return attachmentStream.thumbnailImageSmall(success: async, failure: {})
}
var fullSizedImage: UIImage {
guard let image = attachmentStream.image() else {
guard let image = attachmentStream.originalImage else {
owsFail("\(logTag) in \(#function) unexpectedly unable to build attachment image")
return UIImage()
}

@ -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) {
SwiftAssertIsOnMainThread(#function)
guard let mediaURL = attachmentStream.mediaURL() else {
guard let mediaURL = attachmentStream.originalMediaURL else {
owsFail("\(logTag) in \(#function) mediaURL was unexpectedly nil for attachment: \(attachmentStream)")
return
}

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

@ -316,17 +316,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 {
OWSFail(@"%@ attachment has no file path.", self.logTag);
}
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.originalImage;
}
} 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
{
OWSAssert(stream);
[self showShareUIForURL:stream.mediaURL];
[self showShareUIForURL:stream.originalMediaURL];
}
+ (void)showShareUIForURL:(NSURL *)url

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

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

@ -0,0 +1,175 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import AVFoundation
public enum OWSThumbnailError: Error {
case failure(description: String)
case assertionFailure(description: String)
case externalError(description: String, underlyingError:Error)
}
@objc public class OWSLoadedThumbnail: NSObject {
public typealias DataSourceBlock = () throws -> Data
@objc
public let image: UIImage
let dataSourceBlock: DataSourceBlock
@objc
public init(image: UIImage, filePath: String) {
self.image = image
self.dataSourceBlock = {
return try Data(contentsOf: URL(fileURLWithPath: filePath))
}
}
@objc
public init(image: UIImage, data: Data) {
self.image = image
self.dataSourceBlock = {
return data
}
}
@objc
public func data() throws -> Data {
return try dataSourceBlock()
}
}
private struct OWSThumbnailRequest {
public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void
public typealias FailureBlock = (Error) -> Void
let attachment: TSAttachmentStream
let thumbnailDimensionPoints: UInt
let success: SuccessBlock
let failure: FailureBlock
init(attachment: TSAttachmentStream, thumbnailDimensionPoints: UInt, success: @escaping SuccessBlock, failure: @escaping FailureBlock) {
self.attachment = attachment
self.thumbnailDimensionPoints = thumbnailDimensionPoints
self.success = success
self.failure = failure
}
}
@objc public class OWSThumbnailService: NSObject {
// MARK: - Singleton class
@objc(shared)
public static let shared = OWSThumbnailService()
public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void
public typealias FailureBlock = (Error) -> Void
private let serialQueue = DispatchQueue(label: "OWSThumbnailService")
// This property should only be accessed on the serialQueue.
//
// We want to process requests in _reverse_ order in which they
// arrive so that we prioritize the most recent view state.
private var thumbnailRequestStack = [OWSThumbnailRequest]()
private override init() {
super.init()
SwiftSingletons.register(self)
}
private func canThumbnailAttachment(attachment: TSAttachmentStream) -> Bool {
return attachment.isImage || attachment.isAnimated || attachment.isVideo
}
// completion will only be called on success.
// completion will be called async on the main thread.
@objc
public func ensureThumbnail(forAttachment attachment: TSAttachmentStream,
thumbnailDimensionPoints: UInt,
success: @escaping SuccessBlock,
failure: @escaping FailureBlock) {
serialQueue.async {
let thumbnailRequest = OWSThumbnailRequest(attachment: attachment, thumbnailDimensionPoints: thumbnailDimensionPoints, success: success, failure: failure)
self.thumbnailRequestStack.append(thumbnailRequest)
self.processNextRequestSync()
}
}
private func processNextRequestAsync() {
serialQueue.async {
self.processNextRequestSync()
}
}
// This should only be called on the serialQueue.
private func processNextRequestSync() {
guard let thumbnailRequest = thumbnailRequestStack.popLast() else {
return
}
do {
let loadedThumbnail = try process(thumbnailRequest: thumbnailRequest)
DispatchQueue.main.async {
thumbnailRequest.success(loadedThumbnail)
}
} catch {
Logger.error("Could not create thumbnail: \(error)")
DispatchQueue.main.async {
thumbnailRequest.failure(error)
}
}
}
// This should only be called on the serialQueue.
//
// It should be safe to assume that an attachment will never end up with two thumbnails of
// the same size since:
//
// * Thumbnails are only added by this method.
// * This method checks for an existing thumbnail using the same connection.
// * This method is performed on the serial queue.
private func process(thumbnailRequest: OWSThumbnailRequest) throws -> OWSLoadedThumbnail {
let attachment = thumbnailRequest.attachment
guard canThumbnailAttachment(attachment: attachment) else {
throw OWSThumbnailError.failure(description: "Cannot thumbnail attachment.")
}
let thumbnailPath = attachment.path(forThumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints)
if FileManager.default.fileExists(atPath: thumbnailPath) {
guard let image = UIImage(contentsOfFile: thumbnailPath) else {
throw OWSThumbnailError.failure(description: "Could not load thumbnail.")
}
return OWSLoadedThumbnail(image: image, filePath: thumbnailPath)
}
let thumbnailDirPath = (thumbnailPath as NSString).deletingLastPathComponent
guard OWSFileSystem.ensureDirectoryExists(thumbnailDirPath) else {
throw OWSThumbnailError.failure(description: "Could not create attachment's thumbnail directory.")
}
guard let originalFilePath = attachment.originalFilePath else {
throw OWSThumbnailError.failure(description: "Missing original file path.")
}
let maxDimension = CGFloat(thumbnailRequest.thumbnailDimensionPoints)
let thumbnailImage: UIImage
if attachment.isImage || attachment.isAnimated {
thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension)
} else if attachment.isVideo {
thumbnailImage = try OWSMediaUtils.thumbnail(forVideoAtPath: originalFilePath, maxDimension: maxDimension)
} else {
throw OWSThumbnailError.assertionFailure(description: "Invalid attachment type.")
}
guard let thumbnailData = UIImageJPEGRepresentation(thumbnailImage, 0.85) else {
throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
}
do {
try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath), options: .atomicWrite)
} catch let error as NSError {
throw OWSThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error)
}
return OWSLoadedThumbnail(image: thumbnailImage, data: thumbnailData)
}
}

@ -17,6 +17,9 @@ NS_ASSUME_NONNULL_BEGIN
@class TSAttachmentPointer;
@class YapDatabaseReadWriteTransaction;
typedef void (^OWSThumbnailSuccess)(UIImage *image);
typedef void (^OWSThumbnailFailure)(void);
@interface TSAttachmentStream : TSAttachment
- (instancetype)init NS_UNAVAILABLE;
@ -37,22 +40,21 @@ 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;
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType;
@property (nonatomic, readonly, nullable) UIImage *originalImage;
@property (nonatomic, readonly, nullable) NSString *originalFilePath;
@property (nonatomic, readonly, nullable) NSURL *originalMediaURL;
- (nullable NSString *)filePath;
- (nullable NSString *)thumbnailPath;
- (NSArray<NSString *> *)allThumbnailPaths;
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType;
- (nullable NSData *)readDataFromFileWithError:(NSError **)error;
- (BOOL)writeData:(NSData *)data error:(NSError **)error;
@ -77,6 +79,24 @@ NS_ASSUME_NONNULL_BEGIN
// Non-nil for attachments which need "lazy backup restore."
- (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;

@ -8,13 +8,22 @@
#import "OWSFileSystem.h"
#import "TSAttachmentPointer.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 * kMinZoomFactor, screenSizePoints.height * kMinZoomFactor);
}
typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail);
@interface TSAttachmentStream ()
@ -96,18 +105,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 +156,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
}
self.localRelativeFilePath = localRelativeFilePath;
OWSAssert(self.filePath);
OWSAssert(self.originalFilePath);
}
#pragma mark - File Management
@ -164,7 +164,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) {
OWSFail(@"%@ Missing path for attachment.", self.logTag);
return nil;
@ -177,7 +177,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
OWSAssert(data);
*error = nil;
NSString *_Nullable filePath = self.filePath;
NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) {
OWSFail(@"%@ Missing path for attachment.", self.logTag);
return NO;
@ -190,7 +190,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
{
OWSAssert(dataSource);
NSString *_Nullable filePath = self.filePath;
NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) {
OWSFail(@"%@ Missing path for attachment.", self.logTag);
return NO;
@ -229,7 +229,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return attachmentsFolder;
}
- (nullable NSString *)filePath
- (nullable NSString *)originalFilePath
{
if (!self.localRelativeFilePath) {
OWSFail(@"%@ Attachment missing local file path.", self.logTag);
@ -239,9 +239,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) {
OWSFail(@"%@ Attachment missing local file path.", self.logTag);
return nil;
@ -258,9 +258,28 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return [[containingDir stringByAppendingPathComponent:newFilename] stringByAppendingPathExtension:@"jpg"];
}
- (nullable NSURL *)mediaURL
- (NSString *)thumbnailsDirPath
{
if (!self.localRelativeFilePath) {
OWSFail(@"%@ Attachment missing local file path.", self.logTag);
return nil;
}
// Thumbnails are written to the caches directory, so that iOS can
// remove them if necessary.
NSString *dirName = [NSString stringWithFormat:@"%@-thumbnails", self.uniqueId];
return [OWSFileSystem.cachesDirectoryPath stringByAppendingPathComponent:dirName];
}
- (NSString *)pathForThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
{
NSString *_Nullable filePath = self.filePath;
NSString *filename = [NSString stringWithFormat:@"thumbnail-%lu.jpg", (unsigned long)thumbnailDimensionPoints];
return [self.thumbnailsDirPath stringByAppendingPathComponent:filename];
}
- (nullable NSURL *)originalMediaURL
{
NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) {
OWSFail(@"%@ Missing path for attachment.", self.logTag);
return nil;
@ -272,24 +291,31 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
{
NSError *error;
NSString *_Nullable thumbnailPath = self.thumbnailPath;
if (thumbnailPath) {
[[NSFileManager defaultManager] removeItemAtPath:thumbnailPath error:&error];
NSString *thumbnailsDirPath = self.thumbnailsDirPath;
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailsDirPath]) {
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:thumbnailsDirPath error:&error];
if (error || !success) {
DDLogError(@"%@ remove thumbnails dir failed with: %@", self.logTag, error);
}
}
if (error) {
DDLogError(@"%@ remove thumbnail errored with: %@", self.logTag, error);
NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath;
if (legacyThumbnailPath) {
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:legacyThumbnailPath error:&error];
if (error || !success) {
DDLogError(@"%@ remove legacy thumbnail failed with: %@", self.logTag, error);
}
}
NSString *_Nullable filePath = self.filePath;
NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) {
OWSFail(@"%@ Missing path for attachment.", self.logTag);
return;
}
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (error) {
DDLogError(@"%@ remove file errored with: %@", self.logTag, error);
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (error || !success) {
DDLogError(@"%@ remove file failed with: %@", self.logTag, error);
}
}
@ -321,31 +347,31 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
{
OWSAssert(self.isImage || self.isAnimated);
return [NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType];
return [NSData ows_isValidImageAtPath:self.originalFilePath mimeType:self.contentType];
}
- (BOOL)isValidVideo
{
OWSAssert(self.isVideo);
return [NSData ows_isValidVideoAtURL:self.mediaURL];
return [OWSMediaUtils isValidVideoWithPath:self.originalFilePath];
}
#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 +388,12 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
return nil;
}
if (![NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType]) {
if (![NSData ows_isValidImageAtPath:self.originalFilePath mimeType:self.contentType]) {
OWSFail(@"%@ skipping invalid image", self.logTag);
return nil;
}
return [NSData dataWithContentsOfFile:self.filePath];
return [NSData dataWithContentsOfFile:self.originalFilePath];
}
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType
@ -376,144 +402,17 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
[MIMETypeUtil isAnimated:contentType]);
}
- (nullable UIImage *)thumbnailImage
{
NSString *thumbnailPath = self.thumbnailPath;
if (!thumbnailPath) {
OWSAssert(!self.isImage && !self.isVideo && !self.isAnimated);
return nil;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) {
// This isn't true for some useful edge cases tested by the Debug UI.
DDLogError(@"%@ missing thumbnail for attachmentId: %@", self.logTag, self.uniqueId);
return nil;
}
return [UIImage imageWithContentsOfFile:self.thumbnailPath];
}
- (nullable NSData *)thumbnailData
{
NSString *thumbnailPath = self.thumbnailPath;
if (!thumbnailPath) {
OWSAssert(!self.isImage && !self.isVideo && !self.isAnimated);
return nil;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) {
OWSFail(@"%@ missing thumbnail for attachmentId: %@", self.logTag, self.uniqueId);
return nil;
}
return [NSData dataWithContentsOfFile:self.thumbnailPath];
}
- (void)ensureThumbnail
{
NSString *thumbnailPath = self.thumbnailPath;
if (!thumbnailPath) {
return;
}
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) {
// already exists
return;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:self.mediaURL.path]) {
DDLogError(@"%@ while generating thumbnail, source file doesn't exist: %@", self.logTag, self.mediaURL);
// If we're not lazy-restoring this message, the attachment should exist on disk.
OWSAssert(self.lazyRestoreFragmentId);
return;
}
// TODO proper resolution?
CGFloat thumbnailSize = 200;
UIImage *_Nullable result;
if (self.isImage || self.isAnimated) {
if (![self isValidImage]) {
DDLogWarn(@"%@ skipping thumbnail generation for invalid image at path: %@", self.logTag, self.filePath);
return;
}
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)self.mediaURL, NULL);
OWSAssert(imageSource != NULL);
NSDictionary *imageOptions = @{
(NSString const *)kCGImageSourceCreateThumbnailFromImageIfAbsent : (NSNumber const *)kCFBooleanTrue,
(NSString const *)kCGImageSourceThumbnailMaxPixelSize : @(thumbnailSize),
(NSString const *)kCGImageSourceCreateThumbnailWithTransform : (NSNumber const *)kCFBooleanTrue
};
CGImageRef thumbnail
= CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)imageOptions);
CFRelease(imageSource);
result = [[UIImage alloc] initWithCGImage:thumbnail];
CGImageRelease(thumbnail);
} else if (self.isVideo) {
if (![self isValidVideo]) {
DDLogWarn(@"%@ skipping thumbnail for invalid video at path: %@", self.logTag, self.filePath);
return;
}
result = [self videoStillImageWithMaxSize:CGSizeMake(thumbnailSize, thumbnailSize)];
} else {
OWSFail(@"%@ trying to generate thumnail for unexpected attachment: %@ of type: %@",
self.logTag,
self.uniqueId,
self.contentType);
}
if (result == nil) {
DDLogError(@"Unable to build thumbnail for attachmentId: %@", self.uniqueId);
return;
}
NSData *thumbnailData = UIImageJPEGRepresentation(result, 0.9);
OWSAssert(thumbnailData.length > 0);
DDLogDebug(@"%@ generated thumbnail with size: %lu", self.logTag, (unsigned long)thumbnailData.length);
[thumbnailData writeToFile:thumbnailPath atomically:YES];
}
- (nullable UIImage *)videoStillImage
{
if (![self isValidVideo]) {
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) {
DDLogError(@"Could not generate video still: %@", self.filePath.pathExtension);
NSError *error;
UIImage *_Nullable image = [OWSMediaUtils thumbnailForVideoAtPath:self.originalFilePath
maxDimension:ThumbnailDimensionPointsLarge()
error:&error];
if (error || !image) {
DDLogError(@"Could not create video still: %@.", error);
return nil;
}
return [[UIImage alloc] initWithCGImage:imgRef];
return image;
}
+ (void)deleteAttachments
@ -546,7 +445,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;
}
@ -656,7 +555,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
OWSAssert([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.
@ -665,7 +564,7 @@ const CGFloat kMaxVideoStillSize = 1 * 1024;
if (!error) {
return (CGFloat)[audioPlayer duration];
} else {
DDLogError(@"Could not find audio duration: %@", self.mediaURL);
DDLogError(@"Could not find audio duration: %@", self.originalMediaURL);
return 0;
}
}
@ -727,6 +626,159 @@ 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) {
success(loadedThumbnail.image);
}
failure:failure];
return loadedThumbnail.image;
}
- (nullable OWSLoadedThumbnail *)loadedThumbnailWithThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints
success:(OWSLoadedThumbnailSuccess)success
failure:(OWSThumbnailFailure)failure
{
CGSize originalSize = self.imageSize;
if (originalSize.width < 1 || originalSize.height < 1) {
return nil;
}
if (originalSize.width <= thumbnailDimensionPoints || originalSize.height <= thumbnailDimensionPoints) {
// There's no point in generating a thumbnail if the original is smaller than the
// thumbnail size.
return [[OWSLoadedThumbnail alloc] initWithImage:self.originalImage filePath:self.originalFilePath];
}
NSString *thumbnailPath = [self pathForThumbnailDimensionPoints:thumbnailDimensionPoints];
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailPath]) {
UIImage *_Nullable image = [UIImage imageWithContentsOfFile:thumbnailPath];
if (!image) {
OWSFail(@"couldn't load image.");
return nil;
}
return [[OWSLoadedThumbnail alloc] initWithImage:image filePath:thumbnailPath];
}
[OWSThumbnailService.shared ensureThumbnailForAttachment:self
thumbnailDimensionPoints:thumbnailDimensionPoints
success:success
failure:^(NSError *error) {
DDLogError(@"Failed to create thumbnail: %@", error);
failure();
}];
return nil;
}
- (nullable OWSLoadedThumbnail *)loadedThumbnailSmallSync
{
__block dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block OWSLoadedThumbnail *_Nullable loadedThumbnail = nil;
loadedThumbnail = [self loadedThumbnailWithThumbnailDimensionPoints:kThumbnailDimensionPointsSmall
success:^(OWSLoadedThumbnail *asyncLoadedThumbnail) {
@synchronized(self) {
loadedThumbnail = asyncLoadedThumbnail;
}
dispatch_semaphore_signal(semaphore);
}
failure:^{
dispatch_semaphore_signal(semaphore);
}];
// Wait up to N seconds.
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)));
@synchronized(self) {
return loadedThumbnail;
}
}
- (nullable UIImage *)thumbnailImageSmallSync
{
return [self loadedThumbnailSmallSync].image;
}
- (nullable NSData *)thumbnailDataSmallSync
{
NSError *error;
NSData *_Nullable data = [[self loadedThumbnailSmallSync] dataAndReturnError:&error];
if (error || !data) {
OWSFail(@"Couldn't load thumbnail data: %@", error);
return nil;
}
return data;
}
- (NSArray<NSString *> *)allThumbnailPaths
{
NSMutableArray<NSString *> *result = [NSMutableArray new];
NSString *thumbnailsDirPath = self.thumbnailsDirPath;
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailsDirPath]) {
NSError *error;
NSArray<NSString *> *_Nullable fileNames =
[[NSFileManager defaultManager] contentsOfDirectoryAtPath:thumbnailsDirPath error:&error];
if (error || !fileNames) {
OWSFail(@"contentsOfDirectoryAtPath failed with error: %@", error);
} else {
for (NSString *fileName in fileNames) {
NSString *filePath = [thumbnailsDirPath stringByAppendingPathComponent:fileName];
[result addObject:filePath];
}
}
}
NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath;
if (legacyThumbnailPath && [[NSFileManager defaultManager] fileExistsAtPath:legacyThumbnailPath]) {
[result addObject:legacyThumbnailPath];
}
return result;
}
#pragma mark - Update With... Methods
- (void)markForLazyRestoreWithFragment:(OWSBackupFragment *)lazyRestoreFragment
@ -759,7 +811,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;

@ -235,7 +235,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) {
@ -263,7 +263,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

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

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

@ -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,45 @@
return resized;
}
- (nullable UIImage *)resizedWithMaxDimensionPoints:(CGFloat)maxDimensionPoints
{
CGSize originalSize = self.size;
if (originalSize.width < 1 || originalSize.height < 1) {
DDLogError(@"Invalid original size: %@", NSStringFromCGSize(originalSize));
return nil;
}
CGFloat maxOriginalDimensionPoints = MAX(originalSize.width, originalSize.height);
if (maxOriginalDimensionPoints < maxDimensionPoints) {
// Don't bother scaling an image that is already smaller than the max dimension.
return self;
}
CGSize thumbnailSize = CGSizeZero;
if (originalSize.width > originalSize.height) {
thumbnailSize.width = maxDimensionPoints;
thumbnailSize.height = round(maxDimensionPoints * originalSize.height / originalSize.width);
} else {
thumbnailSize.width = round(maxDimensionPoints * originalSize.width / originalSize.height);
thumbnailSize.height = maxDimensionPoints;
}
if (thumbnailSize.width < 1 || thumbnailSize.height < 1) {
DDLogError(@"Invalid thumbnail size: %@", NSStringFromCGSize(thumbnailSize));
return nil;
}
UIGraphicsBeginImageContext(CGSizeMake(thumbnailSize.width, thumbnailSize.height));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
[self drawInRect:CGRectMake(0, 0, thumbnailSize.width, thumbnailSize.height)];
UIImage *_Nullable resized = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resized;
}
// Source: https://github.com/AliSoftware/UIImage-Resize
- (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 +142,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 +160,7 @@
// we use srcSize (and not dstSize) as the size to specify is in user space (and we use the CTM to apply a
// scaleRatio)
CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, srcSize.width, srcSize.height), imgRef);
UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIImage *_Nullable resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resizedImage;

Loading…
Cancel
Save