mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
image sizing
Validate image sizing
This commit is contained in:
parent
2c8cec183c
commit
e715bf9ea2
12 changed files with 215 additions and 28 deletions
2
Pods
2
Pods
|
@ -1 +1 @@
|
|||
Subproject commit 4c3935aa74dfe52f047d664197bbf3f9d7b50c86
|
||||
Subproject commit 9a3f6876d4a6086d10501383b96fb2d9d47a75b6
|
|
@ -12,6 +12,7 @@
|
|||
#import <AssetsLibrary/AssetsLibrary.h>
|
||||
#import <SignalMessaging/NSString+OWS.h>
|
||||
#import <SignalMessaging/OWSUnreadIndicator.h>
|
||||
#import <SignalServiceKit/NSData+Image.h>
|
||||
#import <SignalServiceKit/OWSContact.h>
|
||||
#import <SignalServiceKit/TSInteraction.h>
|
||||
|
||||
|
@ -482,8 +483,20 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
if ([self.attachmentStream isAnimated]) {
|
||||
self.messageCellType = OWSMessageCellType_AnimatedImage;
|
||||
} else if ([self.attachmentStream isImage]) {
|
||||
if (![self.attachmentStream isValidImage]) {
|
||||
DDLogWarn(@"Treating invalid image as generic attachment.");
|
||||
self.messageCellType = OWSMessageCellType_GenericAttachment;
|
||||
return;
|
||||
}
|
||||
|
||||
self.messageCellType = OWSMessageCellType_StillImage;
|
||||
} else if ([self.attachmentStream isVideo]) {
|
||||
if (![self.attachmentStream isValidVideo]) {
|
||||
DDLogWarn(@"Treating invalid video as generic attachment.");
|
||||
self.messageCellType = OWSMessageCellType_GenericAttachment;
|
||||
return;
|
||||
}
|
||||
|
||||
self.messageCellType = OWSMessageCellType_Video;
|
||||
} else {
|
||||
OWSFail(@"%@ unexpected attachment type.", self.logTag);
|
||||
|
|
|
@ -196,6 +196,11 @@ class GifPickerCell: UICollectionViewCell {
|
|||
clearViewState()
|
||||
return
|
||||
}
|
||||
guard NSData.ows_isValidImage(atPath: asset.filePath, mimeType: OWSMimeTypeImageGif) else {
|
||||
owsFail("\(logTag) invalid asset.")
|
||||
clearViewState()
|
||||
return
|
||||
}
|
||||
guard let image = YYImage(contentsOfFile: asset.filePath) else {
|
||||
owsFail("\(logTag) could not load asset.")
|
||||
clearViewState()
|
||||
|
|
|
@ -207,6 +207,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
|
|||
}
|
||||
|
||||
private func createImagePreview() {
|
||||
guard attachment.isValidImage else {
|
||||
createGenericPreview()
|
||||
return
|
||||
}
|
||||
guard let image = attachment.image() else {
|
||||
createGenericPreview()
|
||||
return
|
||||
|
@ -225,6 +229,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
|
|||
}
|
||||
|
||||
private func createVideoPreview() {
|
||||
guard attachment.isValidVideo else {
|
||||
createGenericPreview()
|
||||
return
|
||||
}
|
||||
guard let image = attachment.videoPreview() else {
|
||||
createGenericPreview()
|
||||
return
|
||||
|
|
|
@ -141,6 +141,11 @@ public class SignalAttachment: NSObject {
|
|||
return dataSource.isValidImage()
|
||||
}
|
||||
|
||||
@objc
|
||||
public var isValidVideo: Bool {
|
||||
return dataSource.isValidVideo()
|
||||
}
|
||||
|
||||
// This flag should be set for text attachments that can be sent as text messages.
|
||||
@objc
|
||||
public var isConvertibleToTextMessage = false
|
||||
|
|
|
@ -198,7 +198,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan
|
|||
avatarImage = [self.cnContactAvatarCache objectForKey:contactId];
|
||||
if (!avatarImage) {
|
||||
NSData *_Nullable avatarData = [self avatarDataForCNContactId:contactId];
|
||||
if (avatarData) {
|
||||
if (avatarData && [avatarData ows_isValidImage]) {
|
||||
avatarImage = [UIImage imageWithData:avatarData];
|
||||
}
|
||||
if (avatarImage) {
|
||||
|
|
|
@ -77,9 +77,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
// Non-nil for attachments which need "lazy backup restore."
|
||||
- (nullable OWSBackupFragment *)lazyRestoreFragment;
|
||||
|
||||
#pragma mark - Image Validation
|
||||
#pragma mark - Validation
|
||||
|
||||
- (BOOL)isValidImage;
|
||||
- (BOOL)isValidVideo;
|
||||
|
||||
#pragma mark - Update With... Methods
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
const CGFloat kMaxVideoStillSize = 1 * 1024;
|
||||
|
||||
@interface TSAttachmentStream ()
|
||||
|
||||
// We only want to generate the file path for this attachment once, so that
|
||||
|
@ -315,14 +317,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
#pragma mark - Image Validation
|
||||
|
||||
- (BOOL)isValidImageWithData:(NSData *)data
|
||||
{
|
||||
OWSAssert(self.isImage || self.isAnimated);
|
||||
OWSAssert(data);
|
||||
|
||||
return [data ows_isValidImageWithMimeType:self.contentType];
|
||||
}
|
||||
|
||||
- (BOOL)isValidImage
|
||||
{
|
||||
OWSAssert(self.isImage || self.isAnimated);
|
||||
|
@ -330,6 +324,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return [NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType];
|
||||
}
|
||||
|
||||
- (BOOL)isValidVideo
|
||||
{
|
||||
OWSAssert(self.isVideo);
|
||||
|
||||
return [NSData ows_isValidVideoAtURL:self.mediaURL];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (nullable UIImage *)image
|
||||
|
@ -341,11 +342,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
if (!mediaUrl) {
|
||||
return nil;
|
||||
}
|
||||
NSData *data = [NSData dataWithContentsOfURL:mediaUrl];
|
||||
if (![self isValidImageWithData:data]) {
|
||||
if (![self isValidImage]) {
|
||||
return nil;
|
||||
}
|
||||
return [UIImage imageWithData:data];
|
||||
return [[UIImage alloc] initWithContentsOfFile:self.filePath];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
|
@ -362,17 +362,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return nil;
|
||||
}
|
||||
|
||||
NSURL *_Nullable mediaUrl = [self mediaURL];
|
||||
if (!mediaUrl) {
|
||||
if (![NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType]) {
|
||||
OWSFail(@"%@ skipping invalid image", self.logTag);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *data = [NSData dataWithContentsOfURL:mediaUrl];
|
||||
if (![self isValidImageWithData:data]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return data;
|
||||
return [NSData dataWithContentsOfFile:self.filePath];
|
||||
}
|
||||
|
||||
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType
|
||||
|
@ -462,6 +457,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
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: %@",
|
||||
|
@ -471,7 +471,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
if (result == nil) {
|
||||
OWSFail(@"%@ Unable to build thumbnail for attachmentId: %@", self.logTag, self.uniqueId);
|
||||
DDLogError(@"Unable to build thumbnail for attachmentId: %@", self.uniqueId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -484,12 +484,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (nullable UIImage *)videoStillImage
|
||||
{
|
||||
if (![self isValidVideo]) {
|
||||
return nil;
|
||||
}
|
||||
// Uses the assets intrinsic size by default
|
||||
return [self videoStillImageWithMaxSize:CGSizeZero];
|
||||
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;
|
||||
|
@ -502,6 +508,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
NSError *err = NULL;
|
||||
CMTime time = CMTimeMake(1, 60);
|
||||
CGImageRef imgRef = [generator copyCGImageAtTime:time actualTime:NULL error:&err];
|
||||
if (imgRef == NULL) {
|
||||
DDLogError(@"Could not generate video still: %@", self.filePath.pathExtension);
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[UIImage alloc] initWithCGImage:imgRef];
|
||||
}
|
||||
|
@ -531,6 +541,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (CGSize)calculateImageSize
|
||||
{
|
||||
if ([self isVideo]) {
|
||||
if (![self isValidVideo]) {
|
||||
return CGSizeZero;
|
||||
}
|
||||
return [self videoStillImage].size;
|
||||
} else if ([self isImage] || [self isAnimated]) {
|
||||
NSURL *_Nullable mediaUrl = [self mediaURL];
|
||||
|
|
|
@ -31,6 +31,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (BOOL)isValidImage;
|
||||
|
||||
- (BOOL)isValidVideo;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
|
|
@ -73,6 +73,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return [data ows_isValidImage];
|
||||
}
|
||||
|
||||
- (BOOL)isValidVideo
|
||||
{
|
||||
return [NSData ows_isValidVideoAtURL:self.dataUrl];
|
||||
}
|
||||
|
||||
- (void)setSourceFilename:(nullable NSString *)sourceFilename
|
||||
{
|
||||
_sourceFilename = sourceFilename.filterFilename;
|
||||
|
|
|
@ -11,4 +11,6 @@
|
|||
- (BOOL)ows_isValidImage;
|
||||
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType;
|
||||
|
||||
+ (BOOL)ows_isValidVideoAtURL:(NSURL *)url;
|
||||
|
||||
@end
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#import "NSData+Image.h"
|
||||
#import "MIMETypeUtil.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, ImageFormat) {
|
||||
ImageFormat_Unknown,
|
||||
|
@ -23,18 +24,127 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
|
|||
|
||||
- (BOOL)ows_isValidImage
|
||||
{
|
||||
return [self ows_isValidImageWithMimeType:nil];
|
||||
if (![self ows_isValidImageWithMimeType:nil]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (![self ows_hasValidImageDimensions]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)ows_isValidImageAtPath:(NSString *)filePath mimeType:(nullable NSString *)mimeType
|
||||
{
|
||||
NSError *error = nil;
|
||||
NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];
|
||||
if (error) {
|
||||
NSData *_Nullable data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];
|
||||
if (!data || error) {
|
||||
DDLogError(@"%@ could not read image data: %@", self.logTag, error);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return [data ows_isValidImageWithMimeType:mimeType];
|
||||
if (![data ows_isValidImageWithMimeType:mimeType]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (![self ows_hasValidImageDimensionsAtPath:filePath]) {
|
||||
DDLogError(@"%@ image had invalid dimensions.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)ows_hasValidImageDimensions
|
||||
{
|
||||
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self, NULL);
|
||||
if (imageSource == NULL) {
|
||||
return NO;
|
||||
}
|
||||
BOOL result = [NSData ows_hasValidImageDimensionWithImageSource:imageSource];
|
||||
CFRelease(imageSource);
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (BOOL)ows_hasValidImageDimensionsAtPath:(NSString *)path
|
||||
{
|
||||
NSURL *url = [NSURL fileURLWithPath:path];
|
||||
if (!url) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
|
||||
if (imageSource == NULL) {
|
||||
return NO;
|
||||
}
|
||||
BOOL result = [self ows_hasValidImageDimensionWithImageSource:imageSource];
|
||||
CFRelease(imageSource);
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (BOOL)ows_hasValidImageDimensionWithImageSource:(CGImageSourceRef)imageSource
|
||||
{
|
||||
OWSAssert(imageSource);
|
||||
|
||||
NSDictionary *imageProperties
|
||||
= (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
|
||||
|
||||
if (!imageProperties) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSNumber *widthNumber = imageProperties[(__bridge NSString *)kCGImagePropertyPixelWidth];
|
||||
if (!widthNumber) {
|
||||
DDLogError(@"widthNumber was unexpectedly nil");
|
||||
return NO;
|
||||
}
|
||||
CGFloat width = widthNumber.floatValue;
|
||||
|
||||
NSNumber *heightNumber = imageProperties[(__bridge NSString *)kCGImagePropertyPixelHeight];
|
||||
if (!heightNumber) {
|
||||
DDLogError(@"heightNumber was unexpectedly nil");
|
||||
return NO;
|
||||
}
|
||||
CGFloat height = heightNumber.floatValue;
|
||||
|
||||
/* The number of bits in each color sample of each pixel. The value of this
|
||||
* key is a CFNumberRef. */
|
||||
NSNumber *depthNumber = imageProperties[(__bridge NSString *)kCGImagePropertyDepth];
|
||||
if (!depthNumber) {
|
||||
DDLogError(@"depthNumber was unexpectedly nil");
|
||||
return NO;
|
||||
}
|
||||
NSUInteger depthBits = depthNumber.unsignedIntegerValue;
|
||||
CGFloat depthBytes = (CGFloat)ceil(depthBits / 8.f);
|
||||
|
||||
/* The color model of the image such as "RGB", "CMYK", "Gray", or "Lab".
|
||||
* The value of this key is CFStringRef. */
|
||||
NSString *colorModel = imageProperties[(__bridge NSString *)kCGImagePropertyColorModel];
|
||||
if (!colorModel) {
|
||||
DDLogError(@"colorModel was unexpectedly nil");
|
||||
return NO;
|
||||
}
|
||||
if (![colorModel isEqualToString:(__bridge NSString *)kCGImagePropertyColorModelRGB]
|
||||
&& ![colorModel isEqualToString:(__bridge NSString *)kCGImagePropertyColorModelGray]) {
|
||||
DDLogError(@"Invalid colorModel: %@", colorModel);
|
||||
return NO;
|
||||
}
|
||||
|
||||
// We only support (A)RGB and (A)Grayscale, so worst case is 4.
|
||||
CGFloat kWorseCastComponentsPerPixel = 4;
|
||||
CGFloat bytesPerPixel = kWorseCastComponentsPerPixel * depthBytes;
|
||||
|
||||
CGFloat kMaxDimension = 2 * 1024;
|
||||
CGFloat kExpectedBytePerPixel = 4;
|
||||
CGFloat kMaxBytes = kMaxDimension * kMaxDimension * kExpectedBytePerPixel;
|
||||
CGFloat actualBytes = width * height * bytesPerPixel;
|
||||
if (actualBytes > kMaxBytes) {
|
||||
DDLogWarn(@"invalid dimensions width: %f, height %f, bytesPerPixel: %f", width, height, bytesPerPixel);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType
|
||||
|
@ -151,4 +261,27 @@ 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
|
||||
|
|
Loading…
Reference in a new issue