session-ios/SessionMessagingKit/Sending & Receiving/Attachments/TSAttachmentStream.m

817 lines
26 KiB
Objective-C

#import "TSAttachmentStream.h"
#import "NSData+Image.h"
#import "TSAttachmentPointer.h"
#import <AVFoundation/AVFoundation.h>
#import <SignalCoreKit/Threading.h>
#import <YapDatabase/YapDatabase.h>
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
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 (NSUInteger)MAX(screenSizePoints.width, screenSizePoints.height) * kMinZoomFactor;
}
typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail);
@interface TSAttachmentStream ()
// We only want to generate the file path for this attachment once, so that
// changes in the file path generation logic don't break existing attachments.
@property (nullable, nonatomic) NSString *localRelativeFilePath;
// These properties should only be accessed while synchronized on self.
@property (nullable, nonatomic) NSNumber *cachedImageWidth;
@property (nullable, nonatomic) NSNumber *cachedImageHeight;
// This property should only be accessed on the main thread.
@property (nullable, nonatomic) NSNumber *cachedAudioDurationSeconds;
@property (atomic, nullable) NSNumber *isValidImageCached;
@property (atomic, nullable) NSNumber *isValidVideoCached;
@end
#pragma mark -
@implementation TSAttachmentStream
- (instancetype)initWithContentType:(NSString *)contentType
byteCount:(UInt32)byteCount
sourceFilename:(nullable NSString *)sourceFilename
caption:(nullable NSString *)caption
albumMessageId:(nullable NSString *)albumMessageId
{
self = [super initWithContentType:contentType
byteCount:byteCount
sourceFilename:sourceFilename
caption:caption
albumMessageId:albumMessageId];
if (!self) {
return self;
}
self.isDownloaded = YES;
// TSAttachmentStream doesn't have any "incoming vs. outgoing"
// state, but this constructor is used only for new outgoing
// attachments which haven't been uploaded yet.
_isUploaded = NO;
_creationTimestamp = [NSDate new];
[self ensureFilePath];
return self;
}
- (instancetype)initWithPointer:(TSAttachmentPointer *)pointer
{
// Once saved, this AttachmentStream will replace the AttachmentPointer in the attachments collection.
self = [super initWithPointer:pointer];
if (!self) {
return self;
}
_contentType = pointer.contentType;
self.isDownloaded = YES;
// TSAttachmentStream doesn't have any "incoming vs. outgoing"
// state, but this constructor is used only for new incoming
// attachments which don't need to be uploaded.
_isUploaded = YES;
self.attachmentType = pointer.attachmentType;
_creationTimestamp = [NSDate new];
[self ensureFilePath];
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (!self) {
return self;
}
// OWS105AttachmentFilePaths will ensure the file path is saved if necessary.
[self ensureFilePath];
// OWS105AttachmentFilePaths will ensure the creation timestamp is saved if necessary.
if (!_creationTimestamp) {
_creationTimestamp = [NSDate new];
}
return self;
}
- (void)upgradeFromAttachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion
{
[super upgradeFromAttachmentSchemaVersion:attachmentSchemaVersion];
if (attachmentSchemaVersion < 3) {
// We want to treat any legacy TSAttachmentStream as though
// they have already been uploaded. If it needs to be reuploaded,
// the OWSUploadingService will update this progress when the
// upload begins.
self.isUploaded = YES;
}
if (attachmentSchemaVersion < 4) {
// Legacy image sizes don't correctly reflect image orientation.
@synchronized(self)
{
self.cachedImageWidth = nil;
self.cachedImageHeight = nil;
}
}
}
- (void)ensureFilePath
{
if (self.localRelativeFilePath) {
return;
}
NSString *attachmentsFolder = [[self class] attachmentsFolder];
NSString *filePath = [MIMETypeUtil filePathForAttachment:self.uniqueId
ofMIMEType:self.contentType
sourceFilename:self.sourceFilename
inFolder:attachmentsFolder];
if (!filePath) {
return;
}
if (![filePath hasPrefix:attachmentsFolder]) {
return;
}
NSString *localRelativeFilePath = [filePath substringFromIndex:attachmentsFolder.length];
if (localRelativeFilePath.length < 1) {
return;
}
self.localRelativeFilePath = localRelativeFilePath;
}
#pragma mark - File Management
- (nullable NSData *)readDataFromFileAndReturnError:(NSError **)error
{
*error = nil;
NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) {
return nil;
}
return [NSData dataWithContentsOfFile:filePath options:0 error:error];
}
- (BOOL)writeData:(NSData *)data error:(NSError **)error
{
*error = nil;
NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) {
return NO;
}
return [data writeToFile:filePath options:0 error:error];
}
- (BOOL)writeDataSource:(DataSource *)dataSource
{
NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) {
return NO;
}
return [dataSource writeToPath:filePath];
}
+ (NSString *)legacyAttachmentsDirPath
{
return [[OWSFileSystem appDocumentDirectoryPath] stringByAppendingPathComponent:@"Attachments"];
}
+ (NSString *)sharedDataAttachmentsDirPath
{
return [[OWSFileSystem appSharedDataDirectoryPath] stringByAppendingPathComponent:@"Attachments"];
}
+ (nullable NSError *)migrateToSharedData
{
return [OWSFileSystem moveAppFilePath:self.legacyAttachmentsDirPath
sharedDataFilePath:self.sharedDataAttachmentsDirPath];
}
+ (NSString *)attachmentsFolder
{
static NSString *attachmentsFolder = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
attachmentsFolder = TSAttachmentStream.sharedDataAttachmentsDirPath;
[OWSFileSystem ensureDirectoryExists:attachmentsFolder];
});
return attachmentsFolder;
}
- (nullable NSString *)originalFilePath
{
if (!self.localRelativeFilePath) {
return nil;
}
return [[[self class] attachmentsFolder] stringByAppendingPathComponent:self.localRelativeFilePath];
}
- (nullable NSString *)legacyThumbnailPath
{
NSString *filePath = self.originalFilePath;
if (!filePath) {
return nil;
}
if (!self.isImage && !self.isVideo && !self.isAnimated) {
return nil;
}
NSString *filename = filePath.lastPathComponent.stringByDeletingPathExtension;
NSString *containingDir = filePath.stringByDeletingLastPathComponent;
NSString *newFilename = [filename stringByAppendingString:@"-signal-ios-thumbnail"];
return [[containingDir stringByAppendingPathComponent:newFilename] stringByAppendingPathExtension:@"jpg"];
}
- (NSString *)thumbnailsDirPath
{
if (!self.localRelativeFilePath) {
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) {
return nil;
}
return [NSURL fileURLWithPath:filePath];
}
- (void)removeFileWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
NSError *error;
NSString *thumbnailsDirPath = self.thumbnailsDirPath;
if ([[NSFileManager defaultManager] fileExistsAtPath:thumbnailsDirPath]) {
[[NSFileManager defaultManager] removeItemAtPath:thumbnailsDirPath error:&error];
}
NSString *_Nullable legacyThumbnailPath = self.legacyThumbnailPath;
if (legacyThumbnailPath) {
[[NSFileManager defaultManager] removeItemAtPath:legacyThumbnailPath error:&error];
}
NSString *_Nullable filePath = self.originalFilePath;
if (!filePath) {
return;
}
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
}
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[super removeWithTransaction:transaction];
[self removeFileWithTransaction:transaction];
}
- (BOOL)isValidVisualMedia
{
if (self.isImage && self.isValidImage) {
return YES;
}
if (self.isVideo && self.isValidVideo) {
return YES;
}
if (self.isAnimated && self.isValidImage) {
return YES;
}
return NO;
}
#pragma mark - Image Validation
- (BOOL)isValidImage
{
BOOL result;
BOOL didUpdateCache = NO;
@synchronized(self) {
if (!self.isValidImageCached) {
self.isValidImageCached = @([NSData ows_isValidImageAtPath:self.originalFilePath
mimeType:self.contentType]);
didUpdateCache = YES;
}
result = self.isValidImageCached.boolValue;
}
if (didUpdateCache) {
[self applyChangeAsyncToLatestCopyWithChangeBlock:^(TSAttachmentStream *latestInstance) {
latestInstance.isValidImageCached = @(result);
}];
}
return result;
}
- (BOOL)isValidVideo
{
BOOL result;
BOOL didUpdateCache = NO;
@synchronized(self) {
if (!self.isValidVideoCached) {
self.isValidVideoCached = @([OWSMediaUtils isValidVideoWithPath:self.originalFilePath]);
didUpdateCache = YES;
}
result = self.isValidVideoCached.boolValue;
}
if (didUpdateCache) {
[self applyChangeAsyncToLatestCopyWithChangeBlock:^(TSAttachmentStream *latestInstance) {
latestInstance.isValidVideoCached = @(result);
}];
}
return result;
}
#pragma mark -
- (nullable UIImage *)originalImage
{
if ([self isVideo]) {
return [self videoStillImage];
} else if ([self isImage] || [self isAnimated]) {
NSURL *_Nullable mediaUrl = self.originalMediaURL;
if (!mediaUrl) {
return nil;
}
if (![self isValidImage]) {
return nil;
}
return [[UIImage alloc] initWithContentsOfFile:self.originalFilePath];
} else {
return nil;
}
}
- (nullable NSData *)validStillImageData
{
if ([self isVideo]) {
return nil;
}
if ([self isAnimated]) {
return nil;
}
if (![NSData ows_isValidImageAtPath:self.originalFilePath mimeType:self.contentType]) {
return nil;
}
return [NSData dataWithContentsOfFile:self.originalFilePath];
}
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType
{
return ([MIMETypeUtil isVideo:contentType] || [MIMETypeUtil isImage:contentType] ||
[MIMETypeUtil isAnimated:contentType]);
}
- (nullable UIImage *)videoStillImage
{
NSError *error;
UIImage *_Nullable image = [OWSMediaUtils thumbnailForVideoAtPath:self.originalFilePath
maxDimension:ThumbnailDimensionPointsLarge()
error:&error];
if (error || !image) {
return nil;
}
return image;
}
+ (void)deleteAttachments
{
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *fileURL = [NSURL fileURLWithPath:self.attachmentsFolder];
NSArray<NSURL *> *contents =
[fileManager contentsOfDirectoryAtURL:fileURL includingPropertiesForKeys:nil options:0 error:&error];
if (error) {
return;
}
for (NSURL *url in contents) {
[fileManager removeItemAtURL:url error:&error];
}
}
- (CGSize)calculateImageSize
{
if ([self isVideo]) {
if (![self isValidVideo]) {
return CGSizeZero;
}
return [self videoStillImage].size;
} else if ([self isImage] || [self isAnimated]) {
// imageSizeForFilePath checks validity.
return [NSData imageSizeForFilePath:self.originalFilePath mimeType:self.contentType];
} else {
return CGSizeZero;
}
}
- (BOOL)shouldHaveImageSize
{
return ([self isVideo] || [self isImage] || [self isAnimated]);
}
- (CGSize)imageSize
{
// Avoid crash in dev mode
// OWSAssertDebug(self.shouldHaveImageSize);
@synchronized(self)
{
if (self.cachedImageWidth && self.cachedImageHeight) {
return CGSizeMake(self.cachedImageWidth.floatValue, self.cachedImageHeight.floatValue);
}
CGSize imageSize = [self calculateImageSize];
if (imageSize.width <= 0 || imageSize.height <= 0) {
return CGSizeZero;
}
self.cachedImageWidth = @(imageSize.width);
self.cachedImageHeight = @(imageSize.height);
[self applyChangeAsyncToLatestCopyWithChangeBlock:^(TSAttachmentStream *latestInstance) {
latestInstance.cachedImageWidth = @(imageSize.width);
latestInstance.cachedImageHeight = @(imageSize.height);
}];
return imageSize;
}
}
- (CGSize)cachedMediaSize
{
@synchronized(self) {
if (self.cachedImageWidth && self.cachedImageHeight) {
return CGSizeMake(self.cachedImageWidth.floatValue, self.cachedImageHeight.floatValue);
} else {
return CGSizeZero;
}
}
}
#pragma mark - Update With...
- (void)applyChangeAsyncToLatestCopyWithChangeBlock:(void (^)(TSAttachmentStream *))changeBlock
{
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSString *collection = [TSAttachmentStream collection];
TSAttachmentStream *latestInstance = [transaction objectForKey:self.uniqueId inCollection:collection];
if (!latestInstance) {
// This attachment has either not yet been saved or has been deleted; do nothing.
// This isn't an error per se, but these race conditions should be
// _very_ rare.
//
// An exception is incoming group avatar updates which we don't ever save.
} else if (![latestInstance isKindOfClass:[TSAttachmentStream class]]) {
// Shouldn't occur
} else {
changeBlock(latestInstance);
[latestInstance saveWithTransaction:transaction];
}
}];
}
#pragma mark -
- (CGFloat)calculateAudioDurationSeconds
{
NSError *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.
return 0.f;
}
[audioPlayer prepareToPlay];
if (!error) {
return (CGFloat)[audioPlayer duration];
} else {
return 0;
}
}
- (CGFloat)audioDurationSeconds
{
if (self.cachedAudioDurationSeconds) {
return self.cachedAudioDurationSeconds.floatValue;
}
CGFloat audioDurationSeconds = [self calculateAudioDurationSeconds];
self.cachedAudioDurationSeconds = @(audioDurationSeconds);
[self applyChangeAsyncToLatestCopyWithChangeBlock:^(TSAttachmentStream *latestInstance) {
latestInstance.cachedAudioDurationSeconds = @(audioDurationSeconds);
}];
return audioDurationSeconds;
}
#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 *thumbnail) {
DispatchMainThreadSafe(^{
success(thumbnail.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) {
// Any time we return nil from this method we have to call the failure handler
// or else the caller waits for an async thumbnail
failure();
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) {
// Any time we return nil from this method we have to call the failure handler
// or else the caller waits for an async thumbnail
failure();
return nil;
}
return [[OWSLoadedThumbnail alloc] initWithImage:image filePath:thumbnailPath];
}
[OWSThumbnailService.shared ensureThumbnailForAttachment:self
thumbnailDimensionPoints:thumbnailDimensionPoints
success:success
failure:^(NSError *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 *thumbnail) {
@synchronized(self) {
asyncLoadedThumbnail = thumbnail;
}
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) {
return nil;
}
return loadedThumbnail.image;
}
- (nullable NSData *)thumbnailDataSmallSync
{
OWSLoadedThumbnail *_Nullable loadedThumbnail = [self loadedThumbnailSmallSync];
if (!loadedThumbnail) {
return nil;
}
NSError *error;
NSData *_Nullable data = [loadedThumbnail dataAndReturnError:&error];
if (error || !data) {
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) {
// Do nothing
} 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
- (nullable TSAttachmentStream *)cloneAsThumbnail
{
if (!self.isValidVisualMedia) {
return nil;
}
NSData *_Nullable thumbnailData = self.thumbnailDataSmallSync;
// Only some media types have thumbnails
if (!thumbnailData) {
return nil;
}
// Copy the thumbnail to a new attachment.
NSString *thumbnailName = [NSString stringWithFormat:@"quoted-thumbnail-%@", self.sourceFilename];
TSAttachmentStream *thumbnailAttachment =
[[TSAttachmentStream alloc] initWithContentType:OWSMimeTypeImageJpeg
byteCount:(uint32_t)thumbnailData.length
sourceFilename:thumbnailName
caption:nil
albumMessageId:nil];
NSError *error;
BOOL success = [thumbnailAttachment writeData:thumbnailData error:&error];
if (!success || error) {
return nil;
}
return thumbnailAttachment;
}
// MARK: Protobuf serialization
+ (nullable SNProtoAttachmentPointer *)buildProtoForAttachmentId:(nullable NSString *)attachmentId
{
// TODO we should past in a transaction, rather than sneakily generate one in `fetch...` to make sure we're
// getting a consistent view in the message sending process. A brief glance shows it touches quite a bit of code,
// but should be straight forward.
TSAttachment *attachment = [TSAttachmentStream fetchObjectWithUniqueID:attachmentId];
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
return nil;
}
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
return [attachmentStream buildProto];
}
- (nullable SNProtoAttachmentPointer *)buildProto
{
SNProtoAttachmentPointerBuilder *builder = [SNProtoAttachmentPointer builderWithId:self.serverId];
builder.contentType = self.contentType;
if (self.sourceFilename.length > 0) {
builder.fileName = self.sourceFilename;
}
if (self.caption.length > 0) {
builder.caption = self.caption;
}
builder.size = self.byteCount;
builder.key = self.encryptionKey;
builder.digest = self.digest;
builder.flags = self.isVoiceMessage ? SNProtoAttachmentPointerFlagsVoiceMessage : 0;
if (self.shouldHaveImageSize) {
CGSize imageSize = self.imageSize;
if (imageSize.width < NSIntegerMax && imageSize.height < NSIntegerMax) {
NSInteger imageWidth = (NSInteger)round(imageSize.width);
NSInteger imageHeight = (NSInteger)round(imageSize.height);
if (imageWidth > 0 && imageHeight > 0) {
builder.width = (UInt32)imageWidth;
builder.height = (UInt32)imageHeight;
}
}
}
builder.url = self.downloadURL;
NSError *error;
SNProtoAttachmentPointer *_Nullable attachmentProto = [builder buildAndReturnError:&error];
if (error || !attachmentProto) {
return nil;
}
return attachmentProto;
}
@end
NS_ASSUME_NONNULL_END