diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 48426240f..b3a974951 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -235,6 +235,7 @@ 34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2A1F74C12700D7438D /* DebugUIStress.m */; }; 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */; }; 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2F1F7ABCF800D7438D /* GifPickerLayout.swift */; }; + 34BEDB0E21C405B0007B0EAE /* ImageEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BEDB0D21C405B0007B0EAE /* ImageEditor.swift */; }; 34C3C78D20409F320000134C /* Opening.m4r in Resources */ = {isa = PBXBuildFile; fileRef = 34C3C78C20409F320000134C /* Opening.m4r */; }; 34C3C78F2040A4F70000134C /* sonarping.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 34C3C78E2040A4F70000134C /* sonarping.mp3 */; }; 34C3C7922040B0DD0000134C /* OWSAudioPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 34C3C7902040B0DC0000134C /* OWSAudioPlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -906,6 +907,7 @@ 34BECE2A1F74C12700D7438D /* DebugUIStress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIStress.m; sourceTree = ""; }; 34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerViewController.swift; sourceTree = ""; }; 34BECE2F1F7ABCF800D7438D /* GifPickerLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerLayout.swift; sourceTree = ""; }; + 34BEDB0D21C405B0007B0EAE /* ImageEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageEditor.swift; sourceTree = ""; }; 34C3C78C20409F320000134C /* Opening.m4r */ = {isa = PBXFileReference; lastKnownFileType = file; path = Opening.m4r; sourceTree = ""; }; 34C3C78E2040A4F70000134C /* sonarping.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = sonarping.mp3; path = Signal/AudioFiles/sonarping.mp3; sourceTree = SOURCE_ROOT; }; 34C3C7902040B0DC0000134C /* OWSAudioPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioPlayer.h; sourceTree = ""; }; @@ -1714,7 +1716,9 @@ 34AC0A00211B39E700997B47 /* DisappearingTimerConfigurationView.swift */, 4CA46F49219C78050038ABDE /* GalleryRailView.swift */, 34AC0A08211B39E900997B47 /* GradientView.swift */, + 34BEDB0C21C405B0007B0EAE /* ImageEditor */, 34AC0A06211B39E900997B47 /* OWSAlerts.swift */, + 4C618198219DF03A009BD6B5 /* OWSButton.swift */, 34AC0A09211B39E900997B47 /* OWSFlatButton.swift */, 34AC09FE211B39E700997B47 /* OWSLayerView.swift */, 34AC0A03211B39E800997B47 /* OWSNavigationBar.swift */, @@ -1729,7 +1733,6 @@ 34AC0A0D211B39EA00997B47 /* ThreadViewHelper.h */, 34AC0A0B211B39EA00997B47 /* ThreadViewHelper.m */, 34AC0A04211B39E800997B47 /* VideoPlayerView.swift */, - 4C618198219DF03A009BD6B5 /* OWSButton.swift */, ); path = Views; sourceTree = ""; @@ -1858,6 +1861,14 @@ path = GifPicker; sourceTree = ""; }; + 34BEDB0C21C405B0007B0EAE /* ImageEditor */ = { + isa = PBXGroup; + children = ( + 34BEDB0D21C405B0007B0EAE /* ImageEditor.swift */, + ); + path = ImageEditor; + sourceTree = ""; + }; 34C3C78B20409F320000134C /* ringtoneSounds */ = { isa = PBXGroup; children = ( @@ -3323,6 +3334,7 @@ 34AC09E9211B39B100997B47 /* OWSTableViewController.m in Sources */, 346129F51FD5F31400532771 /* OWS102MoveLoggingPreferenceToUserDefaults.m in Sources */, 45194F8F1FD71FF500333B2C /* ThreadUtil.m in Sources */, + 34BEDB0E21C405B0007B0EAE /* ImageEditor.swift in Sources */, 451F8A3B1FD71297005CB9DA /* UIUtil.m in Sources */, 450C800F20AD1AB900F3A091 /* OWSWindowManager.m in Sources */, 454A965A1FD6017E008D2A0E /* SignalAttachment.swift in Sources */, diff --git a/SignalMessaging/Views/ImageEditor/ImageEditor.swift b/SignalMessaging/Views/ImageEditor/ImageEditor.swift new file mode 100644 index 000000000..67f742726 --- /dev/null +++ b/SignalMessaging/Views/ImageEditor/ImageEditor.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import UIKit + +@objc public enum ImageEditorError: Int, Error { + case assertionError + case invalidInput +} + +@objc +public class ImageEditorItem: NSObject { + @objc + public let itemId: String + + @objc + public override required init() { + self.itemId = UUID().uuidString + + super.init() + } +} + +@objc +public class ImageEditorModel: NSObject { + private let srcImagePath: String + private let srcImageSize: CGSize + + @objc + public required init(srcImagePath: String) throws { + self.srcImagePath = srcImagePath + + let srcFileName = (srcImagePath as NSString).lastPathComponent + let srcFileExtension = (srcFileName as NSString).pathExtension + guard let mimeType = MIMETypeUtil.mimeType(forFileExtension: srcFileExtension) else { + Logger.error("Couldn't determine MIME type for file.") + throw ImageEditorError.invalidInput + } + guard MIMETypeUtil.isImage(mimeType) else { + Logger.error("Invalid MIME type: \(mimeType).") + throw ImageEditorError.invalidInput + } + + let srcImageSize = NSData.imageSize(forFilePath: srcImagePath, mimeType: mimeType) + guard srcImageSize.width > 0, srcImageSize.height > 0 else { + Logger.error("Couldn't determine image size.") + throw ImageEditorError.invalidInput + } + self.srcImageSize = srcImageSize + + super.init() + } +} diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 554ba88a2..e3b393dad 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -494,68 +494,13 @@ typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail); } return [self videoStillImage].size; } else if ([self isImage] || [self isAnimated]) { - NSURL *_Nullable mediaUrl = self.originalMediaURL; - if (!mediaUrl) { - return CGSizeZero; - } - if (![self isValidImage]) { - return CGSizeZero; - } - - // With CGImageSource we avoid loading the whole image into memory. - CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)mediaUrl, NULL); - if (!source) { - OWSFailDebug(@"Could not load image: %@", mediaUrl); - return CGSizeZero; - } - - NSDictionary *options = @{ - (NSString *)kCGImageSourceShouldCache : @(NO), - }; - NSDictionary *properties - = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, (CFDictionaryRef)options); - CGSize imageSize = CGSizeZero; - if (properties) { - NSNumber *orientation = properties[(NSString *)kCGImagePropertyOrientation]; - NSNumber *width = properties[(NSString *)kCGImagePropertyPixelWidth]; - NSNumber *height = properties[(NSString *)kCGImagePropertyPixelHeight]; - - if (width && height) { - imageSize = CGSizeMake(width.floatValue, height.floatValue); - - if (orientation) { - imageSize = - [self applyImageOrientation:(UIImageOrientation)orientation.intValue toImageSize:imageSize]; - } - } else { - OWSFailDebug(@"Could not determine size of image: %@", mediaUrl); - } - } - CFRelease(source); - return imageSize; + // imageSizeForFilePath checks validity. + return [NSData imageSizeForFilePath:self.originalFilePath mimeType:self.contentType]; } else { return CGSizeZero; } } -- (CGSize)applyImageOrientation:(UIImageOrientation)orientation toImageSize:(CGSize)imageSize -{ - switch (orientation) { - case UIImageOrientationUp: // EXIF = 1 - case UIImageOrientationUpMirrored: // EXIF = 2 - case UIImageOrientationDown: // EXIF = 3 - case UIImageOrientationDownMirrored: // EXIF = 4 - return imageSize; - case UIImageOrientationLeftMirrored: // EXIF = 5 - case UIImageOrientationLeft: // EXIF = 6 - case UIImageOrientationRightMirrored: // EXIF = 7 - case UIImageOrientationRight: // EXIF = 8 - return CGSizeMake(imageSize.height, imageSize.width); - default: - return imageSize; - } -} - - (BOOL)shouldHaveImageSize { return ([self isVideo] || [self isImage] || [self isAnimated]); diff --git a/SignalServiceKit/src/Util/NSData+Image.h b/SignalServiceKit/src/Util/NSData+Image.h index 5c86b2bcc..fccd91df9 100644 --- a/SignalServiceKit/src/Util/NSData+Image.h +++ b/SignalServiceKit/src/Util/NSData+Image.h @@ -11,4 +11,7 @@ - (BOOL)ows_isValidImage; - (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType; +// Returns CGSizeZero on error. ++ (CGSize)imageSizeForFilePath:(NSString *)filePath mimeType:(NSString *)mimeType; + @end diff --git a/SignalServiceKit/src/Util/NSData+Image.m b/SignalServiceKit/src/Util/NSData+Image.m index 9ac1d1482..87169f266 100644 --- a/SignalServiceKit/src/Util/NSData+Image.m +++ b/SignalServiceKit/src/Util/NSData+Image.m @@ -312,4 +312,62 @@ typedef NS_ENUM(NSInteger, ImageFormat) { return (width > 0 && width < kMaxValidSize && height > 0 && height < kMaxValidSize); } ++ (CGSize)imageSizeForFilePath:(NSString *)filePath mimeType:(NSString *)mimeType +{ + if (![NSData ows_isValidImageAtPath:filePath mimeType:mimeType]) { + OWSLogError(@"Invalid image."); + return CGSizeZero; + } + NSURL *url = [NSURL fileURLWithPath:filePath]; + + // With CGImageSource we avoid loading the whole image into memory. + CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)url, NULL); + if (!source) { + OWSFailDebug(@"Could not load image: %@", url); + return CGSizeZero; + } + + NSDictionary *options = @{ + (NSString *)kCGImageSourceShouldCache : @(NO), + }; + NSDictionary *properties + = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, (CFDictionaryRef)options); + CGSize imageSize = CGSizeZero; + if (properties) { + NSNumber *orientation = properties[(NSString *)kCGImagePropertyOrientation]; + NSNumber *width = properties[(NSString *)kCGImagePropertyPixelWidth]; + NSNumber *height = properties[(NSString *)kCGImagePropertyPixelHeight]; + + if (width && height) { + imageSize = CGSizeMake(width.floatValue, height.floatValue); + + if (orientation) { + imageSize = [self applyImageOrientation:(UIImageOrientation)orientation.intValue toImageSize:imageSize]; + } + } else { + OWSFailDebug(@"Could not determine size of image: %@", url); + } + } + CFRelease(source); + return imageSize; +} + ++ (CGSize)applyImageOrientation:(UIImageOrientation)orientation toImageSize:(CGSize)imageSize +{ + switch (orientation) { + case UIImageOrientationUp: // EXIF = 1 + case UIImageOrientationUpMirrored: // EXIF = 2 + case UIImageOrientationDown: // EXIF = 3 + case UIImageOrientationDownMirrored: // EXIF = 4 + return imageSize; + case UIImageOrientationLeftMirrored: // EXIF = 5 + case UIImageOrientationLeft: // EXIF = 6 + case UIImageOrientationRightMirrored: // EXIF = 7 + case UIImageOrientationRight: // EXIF = 8 + return CGSizeMake(imageSize.height, imageSize.width); + default: + return imageSize; + } +} + @end