diff --git a/Classes/ChatConversationTableView.h b/Classes/ChatConversationTableView.h index 0f6162f47..cce625051 100644 --- a/Classes/ChatConversationTableView.h +++ b/Classes/ChatConversationTableView.h @@ -27,7 +27,7 @@ @protocol ChatConversationDelegate -- (BOOL)startImageUpload:(UIImage *)image url:(NSURL *)url withQuality:(float)quality; +- (BOOL)startImageUpload:(UIImage *)image assetId:(NSString *)phAssetId withQuality:(float)quality; - (BOOL)startFileUpload:(NSData *)data withUrl:(NSURL *)url; - (void)resendChat:(NSString *)message withExternalUrl:(NSString *)url; - (void)tableViewIsScrolling; @@ -41,6 +41,7 @@ @property(nonatomic) LinphoneChatRoom *chatRoom; @property(nonatomic, strong) id chatRoomDelegate; +@property NSMutableDictionary *imagesInChatroom; - (void)addEventEntry:(LinphoneEventLog *)event; - (void)scrollToBottom:(BOOL)animated; diff --git a/Classes/ChatConversationTableView.m b/Classes/ChatConversationTableView.m index b3f9de5ff..fe59cc311 100644 --- a/Classes/ChatConversationTableView.m +++ b/Classes/ChatConversationTableView.m @@ -39,29 +39,36 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.tableView.accessibilityIdentifier = @"ChatRoom list"; + _imagesInChatroom = [NSMutableDictionary dictionary]; } #pragma mark - - (void)clearEventList { - [eventList removeAllObjects]; + for (NSValue *value in eventList) { + LinphoneEventLog *event = value.pointerValue; + linphone_event_log_unref(event); + } + [eventList removeAllObjects]; } - (void)updateData { + [self clearEventList]; if (!_chatRoom) return; - [self clearEventList]; + LinphoneChatRoomCapabilitiesMask capabilities = linphone_chat_room_get_capabilities(_chatRoom); bctbx_list_t *chatRoomEvents = (capabilities & LinphoneChatRoomCapabilitiesOneToOne) ? linphone_chat_room_get_history_message_events(_chatRoom, 0) : linphone_chat_room_get_history_events(_chatRoom, 0); - + bctbx_list_t *head = chatRoomEvents; eventList = [[NSMutableArray alloc] initWithCapacity:bctbx_list_size(chatRoomEvents)]; while (chatRoomEvents) { LinphoneEventLog *event = (LinphoneEventLog *)chatRoomEvents->data; [eventList addObject:[NSValue valueWithPointer:linphone_event_log_ref(event)]]; chatRoomEvents = chatRoomEvents->next; } + bctbx_list_free_with_data(head, (bctbx_list_free_func)linphone_event_log_unref); for (FileTransferDelegate *ftd in [LinphoneManager.instance fileTransferDelegates]) { const LinphoneAddress *ftd_peer = @@ -83,7 +90,6 @@ - (void)addEventEntry:(LinphoneEventLog *)event { [eventList addObject:[NSValue valueWithPointer:linphone_event_log_ref(event)]]; int pos = (int)eventList.count - 1; - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:pos inSection:0]; [self.tableView beginUpdates]; [self.tableView insertRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationFade]; @@ -102,12 +108,12 @@ } - (void)scrollToBottom:(BOOL)animated { - [self.tableView reloadData]; + //[self.tableView reloadData]; size_t count = eventList.count; if (!count) return; - [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:(count - 1) inSection:0]]; + //[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:(count - 1) inSection:0]]; [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:(count - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES]; @@ -231,7 +237,6 @@ static const CGFloat MESSAGE_SPACING_PERCENTAGE = 5.f; if (nextEvent) { LinphoneChatMessage *nextChat = linphone_event_log_get_chat_message(nextEvent); if (!linphone_address_equal(linphone_chat_message_get_from_address(nextChat), linphone_chat_message_get_from_address(chat))) { - LOGD(@"BITE"); height += tableView.frame.size.height * MESSAGE_SPACING_PERCENTAGE / 100; } } diff --git a/Classes/ChatConversationView.m b/Classes/ChatConversationView.m index 7e2a3f074..7c43e2936 100644 --- a/Classes/ChatConversationView.m +++ b/Classes/ChatConversationView.m @@ -222,7 +222,7 @@ static UICompositeViewDescription *compositeDescription = nil; //share photo NSData *data = dict[@"nsData"]; UIImage *image = [[UIImage alloc] initWithData:data]; - [self chooseImageQuality:image url:nil]; + [self chooseImageQuality:image assetId:nil]; [defaults removeObjectForKey:@"img"]; } else if (dictWeb) { //share url, if local file, then upload file @@ -310,38 +310,12 @@ static UICompositeViewDescription *compositeDescription = nil; return TRUE; } -- (void)saveAndSend:(UIImage *)image url:(NSURL *)url withQuality:(float)quality{ +- (void)saveAndSend:(UIImage *)image assetId:(NSString *)phAssetId withQuality:(float)quality{ // photo from Camera, must be saved first - if (url == nil) { - __block NSURL *assetURL = nil; - [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ - [PHAssetChangeRequest creationRequestForAssetFromImage:image]; - } completionHandler:^(BOOL success, NSError *error) { - if (success) { - LOGI(@"Image saved to [%@]", [assetURL absoluteString]); - [self startImageUpload:image url:assetURL withQuality:quality]; - } else { - LOGE(@"Cannot save image data downloaded [%@]", [error localizedDescription]); - - UIAlertController *errView = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Transfer error", nil) - message:NSLocalizedString(@"Cannot write image to photo library", - nil) - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) {}]; - - [errView addAction:defaultAction]; - [self presentViewController:errView animated:YES completion:nil]; - } - }]; - } else { - [self startImageUpload:image url:url withQuality:quality]; - } + [self startImageUpload:image assetId:phAssetId withQuality:quality]; } -- (void)chooseImageQuality:(UIImage *)image url:(NSURL *)url { +- (void)chooseImageQuality:(UIImage *)image assetId:(NSString *)phAssetId { DTActionSheet *sheet = [[DTActionSheet alloc] initWithTitle:NSLocalizedString(@"Choose the image size", nil)]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ for (NSString *key in [imageQualities allKeys]) { @@ -351,7 +325,7 @@ static UICompositeViewDescription *compositeDescription = nil; NSString *text = [NSString stringWithFormat:@"%@ (%@)", key, [size toHumanReadableSize]]; [sheet addButtonWithTitle:text block:^() { - [self saveAndSend:image url:url withQuality:[quality floatValue]]; + [self saveAndSend:image assetId:phAssetId withQuality:[quality floatValue]]; }]; } [sheet addCancelButtonWithTitle:NSLocalizedString(@"Cancel", nil) block:nil]; @@ -603,9 +577,9 @@ static UICompositeViewDescription *compositeDescription = nil; #pragma mark ChatRoomDelegate -- (BOOL)startImageUpload:(UIImage *)image url:(NSURL *)url withQuality:(float)quality { +- (BOOL)startImageUpload:(UIImage *)image assetId:(NSString *)phAssetId withQuality:(float)quality { FileTransferDelegate *fileTransfer = [[FileTransferDelegate alloc] init]; - [fileTransfer upload:image withURL:url forChatRoom:_chatRoom withQuality:quality]; + [fileTransfer upload:image withassetId:phAssetId forChatRoom:_chatRoom withQuality:quality]; [_tableController scrollToBottom:true]; return TRUE; } @@ -623,7 +597,7 @@ static UICompositeViewDescription *compositeDescription = nil; #pragma mark ImagePickerDelegate -- (void)imagePickerDelegateImage:(UIImage *)image info:(NSDictionary *)info { +- (void)imagePickerDelegateImage:(UIImage *)image info:(NSString *)phAssetId { // When getting image from the camera, it may be 90° rotated due to orientation // (image.imageOrientation = UIImageOrientationRight). Just rotate it to be face up. if (image.imageOrientation != UIImageOrientationUp) { @@ -637,9 +611,7 @@ static UICompositeViewDescription *compositeDescription = nil; if (IPAD) { [VIEW(ImagePickerView).popoverController dismissPopoverAnimated:TRUE]; } - - NSURL *url = [info valueForKey:UIImagePickerControllerReferenceURL]; - [self chooseImageQuality:image url:url]; + [self chooseImageQuality:image assetId:phAssetId]; } - (void)tableViewIsScrolling { diff --git a/Classes/ImagePickerView.h b/Classes/ImagePickerView.h index 1f03bb5fd..bc02c7a15 100644 --- a/Classes/ImagePickerView.h +++ b/Classes/ImagePickerView.h @@ -21,7 +21,7 @@ @protocol ImagePickerDelegate -- (void)imagePickerDelegateImage:(UIImage *)image info:(NSDictionary *)info; +- (void)imagePickerDelegateImage:(UIImage *)image info:(NSString *)phAssetId; @end diff --git a/Classes/ImagePickerView.m b/Classes/ImagePickerView.m index 5a9d34f50..04e3a58e8 100644 --- a/Classes/ImagePickerView.m +++ b/Classes/ImagePickerView.m @@ -161,27 +161,38 @@ static UICompositeViewDescription *compositeDescription = nil; - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { [self dismiss]; - UIImage *image = [info objectForKey:UIImagePickerControllerEditedImage]; - if (image == nil) { - image = [info objectForKey:UIImagePickerControllerOriginalImage]; - } - if (image != nil) { - if (![info objectForKey:UIImagePickerControllerReferenceURL]) { - //Saving image. Supports picture only, no video - //Maybe add a completion target to send the saved image to, like self, and we would call it manually if the image was not taken. - UIImageWriteToSavedPhotosAlbum(image, self, @selector(savedImage:didFinishSavingWithError:contextInfo:), (__bridge void *)info); - } else { - [self savedImage:image didFinishSavingWithError:nil contextInfo:(__bridge void *)info]; + PHAsset *phasset = [info objectForKey:UIImagePickerControllerPHAsset]; + UIImage *image = [info objectForKey:UIImagePickerControllerEditedImage] ? [info objectForKey:UIImagePickerControllerEditedImage] : [info objectForKey:UIImagePickerControllerOriginalImage]; + if (!phasset) { + __block PHObjectPlaceholder *placeHolder; + [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ + PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAssetFromImage:image]; + placeHolder = [request placeholderForCreatedAsset]; + } completionHandler:^(BOOL success, NSError *error) { + if (success) { + LOGI(@"Image saved to [%@]", [placeHolder localIdentifier]); + [self passImageToDelegate:image PHAssetId:[placeHolder localIdentifier]]; + } else { + LOGE(@"Cannot save image data downloaded [%@]", [error localizedDescription]); + } } + ]; + return; } + [self passImageToDelegate:image PHAssetId:[phasset localIdentifier]]; } -- (void)savedImage:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo { +- (void) passImageToDelegate:(UIImage *)image PHAssetId:(NSString *)assetId { + if (imagePickerDelegate != nil) { + [imagePickerDelegate imagePickerDelegateImage:image info:(NSString *)assetId]; +} +} +/* if (imagePickerDelegate != nil) { [imagePickerDelegate imagePickerDelegateImage:image info:(__bridge NSDictionary *)contextInfo]; } } - +*/ - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [self dismiss]; } @@ -234,35 +245,62 @@ static UICompositeViewDescription *compositeDescription = nil; [PhoneMainView.instance changeCurrentView:view.compositeViewDescription]; } }; - - DTActionSheet *sheet = [[DTActionSheet alloc] initWithTitle:NSLocalizedString(@"Select the source", nil)]; - if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - [sheet addButtonWithTitle:NSLocalizedString(@"Camera", nil) - block:^() { - if([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusAuthorized ){ - if([PHPhotoLibrary authorizationStatus] != PHAuthorizationStatusDenied ){ - block(UIImagePickerControllerSourceTypeCamera); - }else{ - [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Photo's permission", nil) message:NSLocalizedString(@"Photo not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; - } - }else { - [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Camera's permission", nil) message:NSLocalizedString(@"Camera not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; - } - }]; - } - if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { - [sheet addButtonWithTitle:NSLocalizedString(@"Photo library", nil) - block:^() { - if([PHPhotoLibrary authorizationStatus] != PHAuthorizationStatusDenied ){ - block(UIImagePickerControllerSourceTypePhotoLibrary); - }else{ - [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Photo's permission", nil) message:NSLocalizedString(@"Photo not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; - } - }]; - } - [sheet addCancelButtonWithTitle:NSLocalizedString(@"Cancel", nil) block:nil]; - - [sheet showInView:PhoneMainView.instance.view]; + + [PHPhotoLibrary authorizationStatus]; + PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; + + if (status == PHAuthorizationStatusNotDetermined) { + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { + if(status != PHAuthorizationStatusAuthorized) { + [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Photo's permission", nil) message:NSLocalizedString(@"Photo not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; + return; + } + DTActionSheet *sheet = [[DTActionSheet alloc] initWithTitle:NSLocalizedString(@"Select the source", nil)]; + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + [sheet addButtonWithTitle:NSLocalizedString(@"Camera", nil) + block:^() { + if([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] != AVAuthorizationStatusAuthorized ) { + [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Camera's permission", nil) message:NSLocalizedString(@"Camera not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; + return; + } + block(UIImagePickerControllerSourceTypeCamera); + }]; + } + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { + [sheet addButtonWithTitle:NSLocalizedString(@"Photo library", nil) + block:^() { + block(UIImagePickerControllerSourceTypePhotoLibrary); + }]; + } + [sheet addCancelButtonWithTitle:NSLocalizedString(@"Cancel", nil) block:nil]; + + [sheet showInView:PhoneMainView.instance.view]; + }]; + } else if (status == PHAuthorizationStatusAuthorized) { + DTActionSheet *sheet = [[DTActionSheet alloc] initWithTitle:NSLocalizedString(@"Select the source", nil)]; + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + [sheet addButtonWithTitle:NSLocalizedString(@"Camera", nil) + block:^() { + if([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] != AVAuthorizationStatusAuthorized ) { + [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Camera's permission", nil) message:NSLocalizedString(@"Camera not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; + return; + } + block(UIImagePickerControllerSourceTypeCamera); + }]; + } + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { + [sheet addButtonWithTitle:NSLocalizedString(@"Photo library", nil) + block:^() { + block(UIImagePickerControllerSourceTypePhotoLibrary); + }]; + } + [sheet addCancelButtonWithTitle:NSLocalizedString(@"Cancel", nil) block:nil]; + + [sheet showInView:PhoneMainView.instance.view]; + } else { + [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Photo's permission", nil) message:NSLocalizedString(@"Photo not authorized", nil) delegate:nil cancelButtonTitle:nil otherButtonTitles:@"Continue", nil] show]; + return; + } } @end diff --git a/Classes/LinphoneManager.h b/Classes/LinphoneManager.h index 4bec1a351..9dd0454de 100644 --- a/Classes/LinphoneManager.h +++ b/Classes/LinphoneManager.h @@ -20,7 +20,6 @@ #import #import #import -#import #import #import @@ -209,6 +208,8 @@ typedef struct _LinphoneManagerSounds { - (void)checkNewVersion; +- (void)loadAvatar; + @property ProviderDelegate *providerDelegate; @property (readonly) BOOL isTesting; @@ -226,7 +227,6 @@ typedef struct _LinphoneManagerSounds { @property (nonatomic, assign) BOOL speakerEnabled; @property (nonatomic, assign) BOOL bluetoothAvailable; @property (nonatomic, assign) BOOL bluetoothEnabled; -@property (readonly) ALAssetsLibrary *photoLibrary; @property (readonly) NSString* contactSipField; @property (readonly,copy) NSString* contactFilter; @property (copy) void (^silentPushCompletion)(UIBackgroundFetchResult); @@ -239,5 +239,6 @@ typedef struct _LinphoneManagerSounds { @property NSDictionary *pushDict; @property(strong, nonatomic) OrderedDictionary *linphoneManagerAddressBookMap; @property (nonatomic, assign) BOOL contactsUpdated; +@property UIImage *avatar; @end diff --git a/Classes/LinphoneManager.m b/Classes/LinphoneManager.m index 2ff2f66cd..5d0a16ef6 100644 --- a/Classes/LinphoneManager.m +++ b/Classes/LinphoneManager.m @@ -287,6 +287,7 @@ struct codec_name_pref_table codec_pref_table[] = {{"speex", 8000, "speex_8k_pre } [self migrateFromUserPrefs]; + [self loadAvatar]; } return self; } @@ -3076,4 +3077,33 @@ static int comp_call_state_paused(const LinphoneCall *call, const void *param) { const char *curVersionCString = [curVersion cStringUsingEncoding:NSUTF8StringEncoding]; linphone_core_check_for_update(theLinphoneCore, curVersionCString); } + +- (void)loadAvatar { + NSString *assetId = [self lpConfigStringForKey:@"avatar"]; + __block UIImage *ret = nil; + if (assetId) { + PHFetchResult *assets = [PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:assetId] options:nil]; + if (![assets firstObject]) { + LOGE(@"Can't fetch avatar image."); + } + PHAsset *asset = [assets firstObject]; + // load avatar synchronously so that we can return UIIMage* directly - since we are + // only using thumbnail, it must be pretty fast to fetch even without cache. + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = TRUE; + [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:options + resultHandler:^(UIImage *image, NSDictionary * info) { + if (image) + ret = [UIImage UIImageThumbnail:image thumbSize:150]; + else + LOGE(@"Can't read avatar"); + }]; + } + + if (!ret) { + ret = [UIImage imageNamed:@"avatar.png"]; + } + _avatar = ret; +} + @end diff --git a/Classes/LinphoneUI/UIChatBubblePhotoCell.m b/Classes/LinphoneUI/UIChatBubblePhotoCell.m index 65e0d4b9a..0cf7d7ae0 100644 --- a/Classes/LinphoneUI/UIChatBubblePhotoCell.m +++ b/Classes/LinphoneUI/UIChatBubblePhotoCell.m @@ -30,9 +30,8 @@ @implementation UIChatBubblePhotoCell { FileTransferDelegate *_ftd; CGSize imageSize, bubbleSize, videoDefaultSize; - int actualAvailableWidth; ChatConversationTableView *chatTableView; - //CGImageRef displayedImage; + BOOL assetIsLoaded; } #pragma mark - Lifecycle Functions @@ -51,11 +50,10 @@ break; } } - [self setFrame:CGRectMake(0, 0, 5, 100)]; [self addSubview:sub]; chatTableView = VIEW(ChatConversationView).tableController; - actualAvailableWidth = chatTableView.tableView.frame.size.width; videoDefaultSize = CGSizeMake(320, 240); + assetIsLoaded = FALSE; } return self; } @@ -76,6 +74,7 @@ _finalImage.image = nil; _finalImage.hidden = TRUE; _fileTransferProgress.progress = 0; + assetIsLoaded = FALSE; [self disconnectFromFileDelegate]; if (amessage) { @@ -98,11 +97,10 @@ [super setChatMessage:amessage]; } -- (void) loadImageAsset:(ALAsset*) asset thumb:(UIImage *)thumb image:(UIImage *)image { +- (void) loadImageAsset:(PHAsset*) asset image:(UIImage *)image { dispatch_async(dispatch_get_main_queue(), ^{ [_finalImage setImage:image]; - [_messageImageView setImage:thumb]; - [_messageImageView setFullImageUrl:asset]; + [_messageImageView setAsset:asset]; [_messageImageView stopLoading]; _messageImageView.hidden = YES; _imageGestureRecognizer.enabled = YES; @@ -110,13 +108,22 @@ }); } -- (void) loadAsset:(ALAsset*) asset { - UIImage *thumb = [[UIImage alloc] initWithCGImage:[asset thumbnail]]; - ALAssetRepresentation *representation = [asset defaultRepresentation]; - imageSize = [UIChatBubbleTextCell getMediaMessageSizefromOriginalSize:[representation dimensions] withWidth:chatTableView.tableView.frame.size.width]; - CGImageRef tmpImg = [self cropImageFromRepresentation:representation]; - UIImage *image = [[UIImage alloc] initWithCGImage:tmpImg]; - [self loadImageAsset:asset thumb:thumb image:image]; +- (void) loadAsset:(PHAsset *) asset { + LOGD(@"SALOPE"); + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = TRUE; + [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:options + resultHandler:^(UIImage *image, NSDictionary * info) { + if (image) { + imageSize = [UIChatBubbleTextCell getMediaMessageSizefromOriginalSize:[image size] withWidth:chatTableView.tableView.frame.size.width]; + UIImage *newImage = [UIImage UIImageResize:image toSize:imageSize]; + [chatTableView.imagesInChatroom setObject:newImage forKey:[asset localIdentifier]]; + [self loadImageAsset:asset image:newImage]; + } + else { + LOGE(@"Can't read image"); + } + }]; } - (void) loadVideoAsset: (AVAsset *) asset { @@ -139,7 +146,7 @@ UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - [self loadImageAsset:nil thumb:thumb image:image]; + [self loadImageAsset:nil image:image]; // put the play button in the top CGRect newFrame = _playButton.frame; @@ -184,39 +191,21 @@ _cancelButton.hidden = _fileTransferProgress.hidden = _downloadButton.hidden = _playButton.hidden = _fileName.hidden = YES; fullScreenImage = YES; } else { + assetIsLoaded = TRUE; + LOGD(@"POUET"); if (localImage) { // we did not load the image yet, so start doing so if (_messageImageView.image == nil) { - NSURL *imageUrl = [NSURL URLWithString:localImage]; [_messageImageView startLoading]; - __block LinphoneChatMessage *achat = self.message; - [LinphoneManager.instance.photoLibrary assetForURL:imageUrl resultBlock:^(ALAsset *asset) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^(void) { - if (achat != self.message) // Avoid glitch and scrolling - return; - - if (asset) { - [self loadAsset:asset]; - } - else { - [LinphoneManager.instance.photoLibrary - enumerateGroupsWithTypes:ALAssetsGroupAll - usingBlock:^(ALAssetsGroup *group, BOOL *stop) { - [group enumerateAssetsWithOptions:NSEnumerationReverse - usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) { - if([result.defaultRepresentation.url isEqual:imageUrl]) { - [self loadAsset:result]; - *stop = YES; - } - }]; - } - failureBlock:^(NSError *error) { - LOGE(@"Error: Cannot load asset from photo stream - %@", [error localizedDescription]); - }]; - } - }); - } failureBlock:^(NSError *error) { - LOGE(@"Can't read image"); - }]; + PHFetchResult *assets = [PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:localImage] options:nil]; + UIImage *img = [chatTableView.imagesInChatroom objectForKey:localImage]; + if (![assets firstObject]) + return; + PHAsset *asset = [assets firstObject]; + if (img) + [self loadImageAsset:asset image:img]; + else + [self loadAsset:asset]; } } else if (localVideo) { if (_messageImageView.image == nil) { @@ -249,13 +238,6 @@ } } } - // resize image so that it take the full bubble space available - CGRect newFrame = _totalView.frame; - newFrame.origin.x = newFrame.origin.y = 0; - if (!fullScreenImage) { - newFrame.size.height -= _imageSubView.frame.size.height; - } - _messageImageView.frame = newFrame; } - (void)fileErrorBlock { @@ -336,9 +318,18 @@ if (![_messageImageView isLoading]) { ImageView *view = VIEW(ImageView); [PhoneMainView.instance changeCurrentView:view.compositeViewDescription]; - CGImageRef fullScreenRef = [[_messageImageView.fullImageUrl defaultRepresentation] fullScreenImage]; - UIImage *fullScreen = [UIImage imageWithCGImage:fullScreenRef]; - [view setImage:fullScreen]; + PHAsset *asset = [_messageImageView asset]; + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = TRUE; + [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:options + resultHandler:^(UIImage *image, NSDictionary * info) { + if (image) { + [view setImage:image]; + } + else { + LOGE(@"Can't read image"); + } + }]; } } } @@ -399,31 +390,8 @@ } } -+ (CGImageRef)resizeCGImage:(CGImageRef)image toWidth:(int)width andHeight:(int)height { - // create context, keeping original image properties - CGColorSpaceRef colorspace = CGImageGetColorSpace(image); - CGContextRef context = CGBitmapContextCreate(NULL, width, height, - CGImageGetBitsPerComponent(image), - CGImageGetBytesPerRow(image), - colorspace, - CGImageGetAlphaInfo(image)); - CGColorSpaceRelease(colorspace); - - if(context == NULL) - return nil; - - // draw image to context (resizing it) - CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); - // extract resulting image from context - CGImageRef imgRef = CGBitmapContextCreateImage(context); - CGContextRelease(context); - - return imgRef; -} - - (void)layoutSubviews { - [super layoutSubviews]; - //if (!isAssetLoaded) return; + //[super layoutSubviews]; UITableView *tableView = VIEW(ChatConversationView).tableController.tableView; BOOL is_outgoing = linphone_chat_message_is_outgoing(super.message); CGRect bubbleFrame = super.bubbleView.frame; @@ -444,25 +412,6 @@ super.bubbleView.frame = bubbleFrame; } -- (CGImageRef)cropImageFromRepresentation:(ALAssetRepresentation*)rep { - CGImageRef newImage = [rep fullResolutionImage]; - CGSize originalSize = [rep dimensions]; - float originalAspectRatio = originalSize.width / originalSize.height; - // We resize in width and crop in height - if (originalSize.width > imageSize.width) { - int height = imageSize.width / originalAspectRatio; - newImage = [self.class resizeCGImage:newImage toWidth:imageSize.width andHeight:height]; - originalSize.height = height; - } - CGRect cropRect = CGRectMake(0, 0, imageSize.width, imageSize.height); - if (imageSize.height < originalSize.height) cropRect.origin.y = (originalSize.height - imageSize.height)/2; - newImage = CGImageCreateWithImageInRect(newImage, cropRect); - LOGD([NSString stringWithFormat:@"Image size : width = %g, height = %g", imageSize.width, imageSize.height]); - LOGD([NSString stringWithFormat:@"Bubble size : width = %g, height = %g", super.bubbleView.frame.size.width, super.bubbleView.frame.size.height]); - return newImage; -} - - @end diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.m b/Classes/LinphoneUI/UIChatBubbleTextCell.m index b45ce2329..ae71d7b9f 100644 --- a/Classes/LinphoneUI/UIChatBubbleTextCell.m +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.m @@ -235,20 +235,26 @@ NSString *localVideo = [LinphoneManager getMessageAppDataForKey:@"localvideo" inMessage:_message]; NSString *localFile = [LinphoneManager getMessageAppDataForKey:@"localfile" inMessage:_message]; NSString *fileName = localVideo ? localVideo : localFile; - NSURL *imageUrl = [NSURL URLWithString:localImage]; [self onDelete]; if(localImage){ - [LinphoneManager.instance.photoLibrary assetForURL:imageUrl - resultBlock:^(ALAsset *asset) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), - ^(void) { - UIImage *image = [[UIImage alloc] initWithCGImage:[[asset defaultRepresentation] fullResolutionImage]]; - [_chatRoomDelegate startImageUpload:image url:imageUrl withQuality:(uploadQuality ? [uploadQuality floatValue] : 0.9)]; - }); - } - failureBlock:^(NSError *error) { - LOGE(@"Can't read image"); - }]; + PHFetchResult *assets = [PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:localImage] options:nil]; + if (![assets firstObject]) { + return; + } + PHAsset *asset = [assets firstObject]; + PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init]; + options.synchronous = TRUE; + [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:options + resultHandler:^(UIImage *image, NSDictionary * info) { + if (image) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), + ^(void) { + [_chatRoomDelegate startImageUpload:image assetId:localImage withQuality:(uploadQuality ? [uploadQuality floatValue] : 0.9)]; + }); + } else { + LOGE(@"Can't read image"); + } + }]; } else if(fileName) { NSString *filePath = [LinphoneManager documentFile:fileName]; [_chatRoomDelegate startFileUpload:[NSData dataWithContentsOfFile:filePath] withUrl:[NSURL URLWithString:filePath]]; @@ -346,22 +352,16 @@ static const CGFloat CELL_MESSAGE_Y_MARGIN = 52; // 44; size = [self getMediaMessageSizefromOriginalSize:videoSize withWidth:width]; size.height += CELL_MESSAGE_X_MARGIN; } else { - NSURL *imageUrl = [NSURL URLWithString:localImage]; - __block CGSize originalImageSize = CGSizeMake(0, 0); - dispatch_semaphore_t sema = dispatch_semaphore_create(0); - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { - [LinphoneManager.instance.photoLibrary assetForURL:imageUrl - resultBlock:^(ALAsset *asset) { - originalImageSize = [[asset defaultRepresentation] dimensions]; - dispatch_semaphore_signal(sema); - } - failureBlock:^(NSError *error) { - LOGE(@"Can't read image"); - dispatch_semaphore_signal(sema); - }]; - }); - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); - + LOGE(@"LA BITE EN BOIS"); + if (!localImage) { + return CGSizeMake(CELL_MIN_WIDTH, CELL_MIN_HEIGHT); + } + PHFetchResult *assets = [PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:localImage] options:nil]; + if (![assets firstObject]) { + return CGSizeMake(CELL_MIN_WIDTH, CELL_MIN_HEIGHT); + } + PHAsset *asset = [assets firstObject]; + CGSize originalImageSize = CGSizeMake([asset pixelWidth], [asset pixelHeight]); size = [self getMediaMessageSizefromOriginalSize:originalImageSize withWidth:width]; //This fixes the image being too small. I think the issue comes form the fact that the display is retina. This should probably be changed in the future. size.height += CELL_MESSAGE_X_MARGIN; diff --git a/Classes/LinphoneUI/UILoadingImageView.h b/Classes/LinphoneUI/UILoadingImageView.h index be8d0e738..d4caa1794 100644 --- a/Classes/LinphoneUI/UILoadingImageView.h +++ b/Classes/LinphoneUI/UILoadingImageView.h @@ -28,7 +28,7 @@ - (BOOL)isLoading; - (void)stopLoading; -@property(nonatomic, strong) ALAsset *fullImageUrl; +@property(nonatomic, strong) PHAsset *asset; @property (nonatomic, readonly) IBOutlet UIActivityIndicatorView *waitIndicatorView; @end diff --git a/Classes/SideMenuView.m b/Classes/SideMenuView.m index baad5833e..b94151454 100644 --- a/Classes/SideMenuView.m +++ b/Classes/SideMenuView.m @@ -104,7 +104,7 @@ #pragma mark - Image picker delegate -- (void)imagePickerDelegateImage:(UIImage *)image info:(NSDictionary *)info { +- (void)imagePickerDelegateImage:(UIImage *)image info:(NSString *)phAssetId { // When getting image from the camera, it may be 90° rotated due to orientation // (image.imageOrientation = UIImageOrientationRight). Just rotate it to be face up. if (image.imageOrientation != UIImageOrientationUp) { @@ -120,27 +120,10 @@ } else { [PhoneMainView.instance.mainViewController hideSideMenu:NO]; } - - NSURL *url = [info valueForKey:UIImagePickerControllerReferenceURL]; - - // taken from camera, must be saved to device first - if (!url) { - __block NSURL *assetURL = nil; - [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ - [PHAssetChangeRequest creationRequestForAssetFromImage:image]; - } completionHandler:^(BOOL success, NSError *error) { - if (success) { - LOGI(@"Image saved to [%@]", [assetURL absoluteString]); - } else { - LOGE(@"Cannot save image data downloaded [%@]", [error localizedDescription]); - } - [LinphoneManager.instance lpConfigSetString:assetURL.absoluteString forKey:@"avatar"]; - _avatarImage.image = [LinphoneUtils selfAvatar]; - }]; - } else { - [LinphoneManager.instance lpConfigSetString:url.absoluteString forKey:@"avatar"]; - _avatarImage.image = [LinphoneUtils selfAvatar]; - } + + [LinphoneManager.instance lpConfigSetString:phAssetId forKey:@"avatar"]; + _avatarImage.image = [LinphoneUtils selfAvatar]; + [LinphoneManager.instance loadAvatar]; } @end diff --git a/Classes/Utils/FileTransferDelegate.h b/Classes/Utils/FileTransferDelegate.h index 10504b144..34a63df2a 100644 --- a/Classes/Utils/FileTransferDelegate.h +++ b/Classes/Utils/FileTransferDelegate.h @@ -12,7 +12,7 @@ @interface FileTransferDelegate : NSObject -- (void)upload:(UIImage *)image withURL:(NSURL *)url forChatRoom:(LinphoneChatRoom *)chatRoom withQuality:(float)quality; +- (void)upload:(UIImage *)image withassetId:(NSString *)phAssetId forChatRoom:(LinphoneChatRoom *)chatRoom withQuality:(float)quality; - (void)uploadFile:(NSData *)data forChatRoom:(LinphoneChatRoom *)chatRoom withUrl:(NSURL *)url; - (void)cancel; - (BOOL)download:(LinphoneChatMessage *)message; diff --git a/Classes/Utils/FileTransferDelegate.m b/Classes/Utils/FileTransferDelegate.m index 7cd878e71..3256e8993 100644 --- a/Classes/Utils/FileTransferDelegate.m +++ b/Classes/Utils/FileTransferDelegate.m @@ -75,44 +75,44 @@ static void linphone_iphone_file_transfer_recv(LinphoneChatMessage *message, con // chat bubble is aware of the fact that image is being saved to device [LinphoneManager setValueInMessageAppData:@"saving..." forKey:@"localimage" inMessage:message]; - [LinphoneManager.instance.photoLibrary - writeImageToSavedPhotosAlbum:image.CGImage - orientation:(ALAssetOrientation)[image imageOrientation] - completionBlock:^(NSURL *assetURL, NSError *error) { - if (error) { - LOGE(@"Cannot save image data downloaded [%@]", [error localizedDescription]); - [LinphoneManager setValueInMessageAppData:nil forKey:@"localimage" inMessage:message]; - UIAlertController *errView = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Transfer error", nil) - message:NSLocalizedString(@"Cannot write image to photo library", - nil) - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) {}]; - - [errView addAction:defaultAction]; - [PhoneMainView.instance presentViewController:errView animated:YES completion:nil]; - } else { - LOGI(@"Image saved to [%@]", [assetURL absoluteString]); - [LinphoneManager setValueInMessageAppData:[assetURL absoluteString] - forKey:@"localimage" - inMessage:message]; - } - [NSNotificationCenter.defaultCenter - postNotificationName:kLinphoneFileTransferRecvUpdate - object:thiz - userInfo:@{ - @"state" : @(LinphoneChatMessageStateDelivered), // we dont want to - // trigger - // FileTransferDone here - @"image" : image, - @"progress" : @(1.f), - }]; - - [thiz stopAndDestroy]; - CFRelease((__bridge CFTypeRef)thiz); - }]; + __block PHObjectPlaceholder *placeHolder; + [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ + PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAssetFromImage:image]; + placeHolder = [request placeholderForCreatedAsset]; + } completionHandler:^(BOOL success, NSError *error) { + if (success) { + LOGI(@"Image saved to [%@]", [placeHolder localIdentifier]); + [LinphoneManager setValueInMessageAppData:[placeHolder localIdentifier] + forKey:@"localimage" + inMessage:message]; + } else { + LOGE(@"Cannot save image data downloaded [%@]", [error localizedDescription]); + [LinphoneManager setValueInMessageAppData:nil forKey:@"localimage" inMessage:message]; + UIAlertController *errView = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Transfer error", nil) + message:NSLocalizedString(@"Cannot write image to photo library", + nil) + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) {}]; + + [errView addAction:defaultAction]; + [PhoneMainView.instance presentViewController:errView animated:YES completion:nil]; + } + [NSNotificationCenter.defaultCenter + postNotificationName:kLinphoneFileTransferRecvUpdate + object:thiz + userInfo:@{ + @"state" : @(LinphoneChatMessageStateDelivered), + // we dont want to trigger FileTransferDone here + @"image" : image, + @"progress" : @(1.f), + }]; + + [thiz stopAndDestroy]; + CFRelease((__bridge CFTypeRef)thiz); + }]; } else { [[LinphoneManager.instance fileTransferDelegates] removeObject:thiz]; @@ -224,10 +224,10 @@ static LinphoneBuffer *linphone_iphone_file_transfer_send(LinphoneChatMessage *m } } -- (void)upload:(UIImage *)image withURL:(NSURL *)url forChatRoom:(LinphoneChatRoom *)chatRoom withQuality:(float)quality { +- (void)upload:(UIImage *)image withassetId:(NSString *)phAssetId forChatRoom:(LinphoneChatRoom *)chatRoom withQuality:(float)quality { NSString *name = [NSString stringWithFormat:@"%li-%f.jpg", (long)image.hash, [NSDate timeIntervalSinceReferenceDate]]; - if (url) - [self uploadData:UIImageJPEGRepresentation(image, quality) forChatRoom:chatRoom type:@"image" subtype:@"jpeg" name:name key:@"localimage" keyData:[url absoluteString] qualityData:[NSNumber numberWithFloat:quality]]; + if (phAssetId) + [self uploadData:UIImageJPEGRepresentation(image, quality) forChatRoom:chatRoom type:@"image" subtype:@"jpeg" name:name key:@"localimage" keyData:phAssetId qualityData:[NSNumber numberWithFloat:quality]]; else [self uploadData:UIImageJPEGRepresentation(image, quality) forChatRoom:chatRoom type:@"image" subtype:@"jpeg" name:name key:@"localimage" keyData:nil qualityData:nil]; } diff --git a/Classes/Utils/Utils.h b/Classes/Utils/Utils.h index ced7d6937..9ac9722bf 100644 --- a/Classes/Utils/Utils.h +++ b/Classes/Utils/Utils.h @@ -102,6 +102,16 @@ typedef enum { @end +@interface UIImage (ResizeAndThumbnail) + ++ (UIImage *)UIImageThumbnail:(UIImage *)image thumbSize:(CGFloat) tbSize; + ++ (UIImage *)UIImageResize:(UIImage *)image toSize:(CGSize) newSize; + ++ (CGImageRef)resizeCGImage:(CGImageRef)image toWidth:(int)width andHeight:(int)height; + +@end + /* Use that macro when you want to invoke a custom initialisation method on your class, whatever is using it (xib, source code, etc., tableview cell) */ #define INIT_WITH_COMMON_C \ diff --git a/Classes/Utils/Utils.m b/Classes/Utils/Utils.m index 67e226e32..3123b02ac 100644 --- a/Classes/Utils/Utils.m +++ b/Classes/Utils/Utils.m @@ -32,36 +32,10 @@ @implementation LinphoneUtils + (BOOL)hasSelfAvatar { - return [NSURL URLWithString:[LinphoneManager.instance lpConfigStringForKey:@"avatar"]] != nil; + return [LinphoneManager.instance lpConfigStringForKey:@"avatar"] != nil; } + (UIImage *)selfAvatar { - NSURL *url = [NSURL URLWithString:[LinphoneManager.instance lpConfigStringForKey:@"avatar"]]; - __block UIImage *ret = nil; - if (url) { - __block NSConditionLock *photoLock = [[NSConditionLock alloc] initWithCondition:1]; - // load avatar synchronously so that we can return UIIMage* directly - since we are - // only using thumbnail, it must be pretty fast to fetch even without cache. - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [LinphoneManager.instance.photoLibrary assetForURL:url - resultBlock:^(ALAsset *asset) { - ret = [[UIImage alloc] initWithCGImage:[asset thumbnail]]; - [photoLock lock]; - [photoLock unlockWithCondition:0]; - } - failureBlock:^(NSError *error) { - LOGE(@"Can't read avatar"); - [photoLock lock]; - [photoLock unlockWithCondition:0]; - }]; - }); - [photoLock lockWhenCondition:0]; - [photoLock unlock]; - } - - if (!ret) { - ret = [UIImage imageNamed:@"avatar.png"]; - } - return ret; + return [LinphoneManager.instance avatar]; } + (NSString *)durationToString:(int)duration { @@ -777,3 +751,91 @@ } @end + +@implementation UIImage (ResizeAndThumbnail) + ++ (UIImage *)UIImageThumbnail:(UIImage *)image thumbSize:(CGFloat) tbSize { + // Create a thumbnail version of the image for the event object. + CGSize size = image.size; + CGSize croppedSize; + CGFloat offsetX = 0.0; + CGFloat offsetY = 0.0; + CGFloat actualTbSize = MAX(tbSize, MAX(size.height, size.width)); + // check the size of the image, we want to make it + // a square with sides the size of the smallest end + if (size.width > size.height) { + offsetX = (size.height - size.width) / 2; + croppedSize = CGSizeMake(size.height, size.height); + } else { + offsetY = (size.width - size.height) / 2; + croppedSize = CGSizeMake(size.width, size.width); + } + + // Crop the image before resize + CGRect clippedRect = CGRectMake(offsetX * -1, + offsetY * -1, + croppedSize.width, + croppedSize.height); + CGImageRef imageRef = CGImageCreateWithImageInRect([image CGImage], + clippedRect); + + UIImage *cropped = [UIImage imageWithCGImage:imageRef]; + CGImageRelease(imageRef); + // Done cropping + + // Resize the image + CGRect rect = CGRectMake(0, 0, actualTbSize, actualTbSize); + + UIGraphicsBeginImageContext(rect.size); + [cropped drawInRect:rect]; + UIImage *thumbnail = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + // Done Resizing + + return thumbnail; +} + + ++ (UIImage *)UIImageResize:(UIImage *)image toSize:(CGSize) newSize { + CGImageRef newImage = [image CGImage]; + CGSize originalSize = [image size]; + float originalAspectRatio = originalSize.width / originalSize.height; + // We resize in width and crop in height + if (originalSize.width > newSize.width) { + int height = newSize.width / originalAspectRatio; + newImage = [UIImage resizeCGImage:newImage toWidth:newSize.width andHeight:height]; + originalSize.height = height; + } + CGRect cropRect = CGRectMake(0, 0, newSize.width, newSize.height); + if (newSize.height < originalSize.height) cropRect.origin.y = (originalSize.height - newSize.height)/2; + newImage = CGImageCreateWithImageInRect(newImage, cropRect); + + + UIImage *cropped = [UIImage imageWithCGImage:newImage]; + CGImageRelease(newImage); + return cropped; +} + ++ (CGImageRef)resizeCGImage:(CGImageRef)image toWidth:(int)width andHeight:(int)height { + // create context, keeping original image properties + CGColorSpaceRef colorspace = CGImageGetColorSpace(image); + CGContextRef context = CGBitmapContextCreate(NULL, width, height, + CGImageGetBitsPerComponent(image), + CGImageGetBytesPerRow(image), + colorspace, + CGImageGetAlphaInfo(image)); + CGColorSpaceRelease(colorspace); + + if(context == NULL) + return nil; + + // draw image to context (resizing it) + CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); + // extract resulting image from context + CGImageRef imgRef = CGBitmapContextCreateImage(context); + CGContextRelease(context); + + return imgRef; +} + +@end